diff --git a/.env.example b/.env.example index e3559db..4b6f0b4 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,13 @@ DATABASE_URL=postgres://ontology:ontology@localhost:55432/ontology REDIS_HOST=localhost REDIS_PORT=56379 +# --- 임베딩 --- +# local : 로컬 multilingual-e5-small (384차원, 최초 1회 모델 다운로드 후 오프라인) +# mock : 문자 bigram 해싱 — 의미는 못 잡음 +# openai : text-embedding-3-small (dimensions=384 로 요청) +EMBEDDING_PROVIDER=local +EMBEDDING_LOCAL_MODEL=Xenova/multilingual-e5-small + # --- LLM --- # mock : API 키 없이 로컬에서 전체 파이프라인 동작 (기본값) # openai : 실제 OpenAI 호출 @@ -17,8 +24,9 @@ OPENAI_MODEL=gpt-4.1-mini OPENAI_EMBEDDING_MODEL=text-embedding-3-small # --- 생성/중복제거 튜닝 --- -# 코사인 유사도가 이 값 이상이면 기존 키워드에 alias 로 흡수 -DEDUP_COSINE_THRESHOLD=0.92 +# 코사인 자동 병합 임계값. 짧은 한글 키워드는 같은 도메인이면 0.93+ 가 기본으로 나오므로 +# 0.92 는 오병합을 부른다. 실측상 어순 변형만 0.999 대에 모이므로 0.99 로 둔다. +DEDUP_COSINE_THRESHOLD=0.99 # trigram 유사도 사전 필터 DEDUP_TRIGRAM_THRESHOLD=0.6 # 벡터 비교 대상 상위 후보 수 diff --git a/README.md b/README.md index 280d18a..6c3bf47 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ o2o-site-AEO 가 발행한 사이트에 **업체별 SEO/AEO 키워드**를 제공하는 온톨로지 서비스. -- 주기적으로 LLM 에게 업체 정보를 주고 키워드·태그·Q&A 를 생성 -- **4단계 계단식 중복제거**로 전역 키워드 사전을 오염 없이 유지 -- 발행 사이트는 REST 로 SEO/AEO payload 만 받아 쓴다 (JSON-LD 조립은 후속 단계) +- **고정 데이터셋 1회 적재** 정책 — 주기 수집 없음 (`data/gunsan-pension-keywords.json`, 1,000건) +- 로컬 임베딩(`multilingual-e5-small`, 384차원)으로 pgvector 에 적재 후 의미 검색 +- 업체명 또는 자연어 문장 → 사전에서 잘 맞는 키워드를 골라주는 **매칭 API + 데모 콘솔** +- 어휘 단계 중복제거는 자동, 벡터 근접쌍은 자동 병합하지 않고 검토 목록으로만 ## 빠른 시작 (로컬) @@ -14,10 +15,15 @@ cp .env.example .env # 기본값은 LLM_PROVIDER=mock — API 키 불 npm run db:up # postgres(pgvector) + redis npm run db:migrate npm run db:seed # 업종/지역 계층 + 데모 업체 3곳 +npm run dataset:build # 1,000건 데이터셋 생성 → data/ +npm run dataset:ingest # 임베딩 + pgvector 적재 (최초 1회 모델 다운로드) npm start # http://localhost:3100 -npm run smoke # (다른 터미널) 엔드투엔드 점검 ``` +브라우저에서 **http://localhost:3100/demo** 를 열면 매칭 콘솔이 뜬다. +입력창에 `스테이 머뭄` 을 넣으면 (띄어쓰기가 달라도) 업체를 해석하고 +적재된 971건 사전에서 잘 맞는 키워드를 순위대로 보여준다. + `npm run db:reset` 은 볼륨까지 지우고 migrate + seed 를 다시 돌린다. ### 실제 OpenAI 로 전환 @@ -48,6 +54,52 @@ OPENAI_EMBEDDING_MODEL=text-embedding-3-small 키워드는 업체에 복제하지 않고 전역 사전 + 연결 테이블로 둔다. 그래야 임베딩이 하나만 저장되고, `강남 미용실` 을 쓰는 업체가 100곳이어도 중복제거가 성립한다. +## 데이터셋 + +`data/gunsan-pension-keywords.json` — "군산 펜션" 주제로 직접 작성한 1,000건. +실제 군산 지명(선유도·고군산군도·새만금·은파호수공원·경암동 철길마을 …)과 +숙박 시설 용어를 어휘로 두고, 한국 로컬 숙박 검색에서 실제로 쓰이는 패턴만 전개했다. + +| 카테고리 | 건수 | 예시 | +|---|---:|---| +| 롱테일 | 374 | 군산 커플 오션뷰 펜션 | +| 시설 | 104 | 군산 자쿠지 펜션 | +| 권역 | 99 | 선유도 독채펜션 | +| 동반자 | 98 | 군산 애견동반 펜션 | +| 시즌 | 72 | 군산 여름휴가 펜션 | +| 관광지 | 64 | 경암동 철길마을 근처 숙소 | +| 태그 | 63 | 오션뷰 · 불멍 · 애견운동장 | +| 코어 | 51 | 군산 펜션 추천 | +| 질문형 | 33 | 군산 펜션 바베큐 가능한가요 | +| 의도 | 13 | 군산 펜션 실시간예약 | + +적재 결과: 1,000건 → 어휘 중복 27건 병합, 금칙어 2건 차단 → **971건 적재**. + +`npm run dataset:build` 로 다시 만들고 `npm run dataset:ingest` 로 다시 넣는다. +재적재는 `ON CONFLICT (normalized, locale) DO UPDATE` 라 몇 번을 돌려도 971건을 유지한다. + +## 임베딩 임계값 — 실측으로 정정한 부분 + +설계 초안의 코사인 자동 병합 임계값 0.92 는 **틀렸다.** 짧은 한글 키워드에서는 +같은 도메인이기만 하면 절대 코사인이 기본적으로 높게 나온다. + +| 쌍 | 실제 관계 | cos (e5-small) | +|---|---|---:| +| 군산 키즈룸 펜션 ↔ 군산 펜션 키즈룸 | 중복 (어순) | **0.9995** | +| 군산 애견동반 펜션 ↔ 군산 반려견 동반 펜션 | 중복 (동의어) | 0.9886 | +| 선유도 펜션 ↔ 선유도 팬션 | 중복 (오타) | 0.9585 | +| 군산 펜션 ↔ 군산 호텔 | **별개** | 0.9698 | +| 선유도 펜션 ↔ 새만금 펜션 | **별개** | 0.9356 | + +중복과 별개의 분포가 겹치므로 단일 임계값으로는 깨끗하게 못 가른다 +(`paraphrase-multilingual-MiniLM-L12-v2` 도 동일). + +그래서 정책을 이렇게 바꿨다. + +- **자동 병합의 주력은 어휘 단계(1~2)** — 공백/구두점 정규화와 `pg_trgm` 이 오타·표기 변형을 잡는다 +- **벡터 단계는 0.99 로 올려 잡는다** — 어순 변형처럼 확실한 것만 걸린다 +- **적재 시에는 벡터 병합을 아예 하지 않고 검토 목록만 출력한다** (`npm run dataset:ingest` 끝부분) + ## 중복제거 4단계 값비싼 벡터 비교를 마지막에 두고, 후보 집합 안에서만 수행한다. @@ -75,7 +127,9 @@ 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/keywords/search` | 의미 기반 키워드 검색 (어드민) | +| `GET` | `/demo` | 매칭 콘솔 (로컬 확인용) | | `POST` | `/v1/sites/:id/performance` | Search Console·유입 로그 피드백 → 저성과 키워드 강등 | `:id` 는 `external_id` 또는 내부 UUID 둘 다 받는다. @@ -110,6 +164,36 @@ curl 'http://localhost:3100/v1/sites/site-1001/seo?limit=8' } ``` +### 매칭 예시 + +```bash +curl -s -X POST http://localhost:3100/v1/match \ + -H 'content-type: application/json' -d '{"query":"스테이 머뭄","limit":5}' +``` + +업체명이면 상호만으로 임베딩하지 않고 **프로필 전체를 질의문으로 조립**한다. +상호는 브랜드명이라 그것만으로는 매칭이 얕아지기 때문이다. + +``` +해석: 스테이머뭄 (군산 / 펜션) ← "스테이 머뭄" 과 띄어쓰기가 달라도 해석됨 +질의문: 스테이머뭄 군산 펜션 고군산군도 초입에 자리한 독채 펜션 … 오션뷰 애견동반 … + +0.8765 군산 독채펜션 예약 [transactional] 코어 +0.8762 군산 애견동반 독채펜션 [local] 동반자 +0.8751 고군산군도 독채펜션 [local] 권역 +0.8735 군산 바베큐 펜션 예약 [transactional] 시설 +0.8718 군산 오션뷰 펜션 예약 [transactional] 시설 +``` + +업체가 해석되지 않으면 입력 문장을 그대로 질의로 쓴다. + +``` +"선유도 근처에서 바베큐 되는 독채" + 0.9166 선유도 독채펜션 + 0.9155 선유도 바베큐 펜션 + 0.9051 선유도해수욕장 근처 숙소 +``` + ## 생성 주기 - **발행 즉시** — `/v1/merchants/publish` 가 BullMQ 에 적재 (60초 dedupe 창) diff --git a/data/gunsan-pension-keywords.json b/data/gunsan-pension-keywords.json new file mode 100644 index 0000000..616bf31 --- /dev/null +++ b/data/gunsan-pension-keywords.json @@ -0,0 +1,7008 @@ +{ + "topic": "군산 펜션", + "locale": "ko-KR", + "generatedBy": "hand-authored vocabulary × search-pattern expansion", + "count": 1000, + "items": [ + { + "keyword": "군산 펜션", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 스파펜션", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 애견펜션", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 감성펜션", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.97 + }, + { + "keyword": "군산 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 숙소 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 숙소 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 숙소 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 숙소 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 숙소 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 풀빌라 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 풀빌라 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 풀빌라 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 풀빌라 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 풀빌라 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 독채펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 독채펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 독채펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 독채펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 독채펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 스파펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 스파펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 스파펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 스파펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 스파펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 애견펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 애견펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 애견펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 애견펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 애견펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 감성펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 감성펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 감성펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 감성펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 감성펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "코어", + "relevance": 0.93 + }, + { + "keyword": "군산 글램핑", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 카라반", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 캠핑장", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 게스트하우스", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 민박", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 리조트", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 한옥펜션", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 촌집", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 별장", + "intent": "local", + "kind": "keyword", + "category": "코어", + "relevance": 0.86 + }, + { + "keyword": "군산 펜션 저렴한곳", + "intent": "local", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 가성비", + "intent": "local", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 최저가", + "intent": "transactional", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 실시간예약", + "intent": "transactional", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 당일예약", + "intent": "transactional", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 특가", + "intent": "transactional", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 할인", + "intent": "transactional", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 위치", + "intent": "local", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 주차", + "intent": "local", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 전화번호", + "intent": "local", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 체크인시간", + "intent": "informational", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 조식포함", + "intent": "local", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "군산 펜션 청소상태", + "intent": "informational", + "kind": "keyword", + "category": "의도", + "relevance": 0.88 + }, + { + "keyword": "선유도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "선유도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "선유도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "선유도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "무녀도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "무녀도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "무녀도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "무녀도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "장자도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "장자도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "장자도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "장자도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "대장도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "대장도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "대장도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "대장도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "신시도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "신시도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "신시도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "신시도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "야미도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "야미도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "야미도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "야미도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "고군산군도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "고군산군도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "고군산군도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "고군산군도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.9 + }, + { + "keyword": "새만금 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "새만금 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "새만금 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "새만금 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "비응항 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "비응항 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "비응항 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "비응항 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "오식도 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "오식도 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "오식도 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "오식도 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "은파호수공원 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "은파호수공원 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "은파호수공원 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "은파호수공원 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "월명동 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "월명동 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "월명동 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "월명동 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "나운동 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "나운동 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "나운동 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "나운동 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "수송동 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "수송동 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "수송동 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "수송동 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "미룡동 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "미룡동 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "미룡동 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "미룡동 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "옥도면 펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "옥도면 숙소", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "옥도면 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "옥도면 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.85 + }, + { + "keyword": "선유도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "선유도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "선유도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "선유도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "선유도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "무녀도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "무녀도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "무녀도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "무녀도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "무녀도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "장자도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "장자도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "장자도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "장자도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "장자도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "대장도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "대장도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "대장도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "대장도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "대장도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "신시도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "신시도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "신시도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "신시도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "신시도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "야미도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "야미도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "야미도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "야미도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "야미도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "고군산군도 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "고군산군도 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "고군산군도 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "고군산군도 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "고군산군도 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "권역", + "relevance": 0.82 + }, + { + "keyword": "군산 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.91 + }, + { + "keyword": "군산 커플 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 커플 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 커플 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 커플 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 커플 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 커플 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 커플 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 커플 스파펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 커플 애견펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.91 + }, + { + "keyword": "군산 가족 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 가족 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 가족 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 가족 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 가족 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 가족 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 가족 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 가족 스파펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 가족 애견펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.91 + }, + { + "keyword": "군산 친구 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 친구 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 친구 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 친구 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 친구 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 친구 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 친구 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 친구 스파펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 친구 애견펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.91 + }, + { + "keyword": "군산 애견동반 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 애견동반 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 애견동반 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 애견동반 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 애견동반 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 애견동반 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 애견동반 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 애견동반 스파펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 애견동반 애견펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.91 + }, + { + "keyword": "군산 단체 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 단체 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 단체 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 단체 펜션 후기", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 단체 펜션 순위", + "intent": "informational", + "kind": "keyword", + "category": "동반자", + "relevance": 0.8 + }, + { + "keyword": "군산 단체 풀빌라", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 단체 독채펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 단체 스파펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 단체 애견펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.78 + }, + { + "keyword": "군산 신혼 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 신혼 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 신혼 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 아이동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 아이동반 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 아이동반 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 유아동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 유아동반 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 유아동반 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 부모님 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 부모님 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 부모님 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 효도여행 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 효도여행 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 효도여행 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 대학생 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 대학생 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 대학생 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 MT 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 MT 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 MT 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 워크샵 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 워크샵 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 워크샵 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 회사 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 회사 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 회사 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 태교여행 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 태교여행 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 태교여행 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 혼자 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 혼자 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 혼자 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 여자끼리 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 여자끼리 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 여자끼리 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 4인 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 4인 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 4인 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 6인 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 6인 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 6인 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 10인 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 10인 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 10인 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 20인 펜션", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.82 + }, + { + "keyword": "군산 20인 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.75 + }, + { + "keyword": "군산 20인 숙소", + "intent": "local", + "kind": "keyword", + "category": "동반자", + "relevance": 0.73 + }, + { + "keyword": "군산 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 오션뷰", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 오션뷰 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 오션뷰 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 오션뷰 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 바다뷰", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 바다뷰 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 바다뷰 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 바다뷰 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 독채", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 독채 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 독채 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 독채 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 프라이빗", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 프라이빗 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 프라이빗 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 프라이빗 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 스파", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 스파 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 스파 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 스파 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 자쿠지", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 자쿠지 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 자쿠지 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 자쿠지 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 바베큐", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 바베큐 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 바베큐 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 바베큐 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.89 + }, + { + "keyword": "군산 펜션 수영장", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.76 + }, + { + "keyword": "군산 수영장 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 수영장 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 수영장 펜션 가격", + "intent": "transactional", + "kind": "keyword", + "category": "시설", + "relevance": 0.72 + }, + { + "keyword": "군산 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 노을뷰", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 일출뷰", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 월풀", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 온수풀", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 야외수영장", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 인피니티풀", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 불멍", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 화로대", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 넷플릭스", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 빔프로젝터", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 노래방", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 파티룸", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 복층", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 테라스", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 마당", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 벽난로", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 애견운동장", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 키즈룸", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 트램폴린", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 무료주차", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 조식", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "군산 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.79 + }, + { + "keyword": "군산 펜션 세미나실", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.7 + }, + { + "keyword": "선유도 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "선유도 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "무녀도 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "장자도 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "대장도 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "시설", + "relevance": 0.74 + }, + { + "keyword": "군산 여름휴가 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 여름휴가 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "여름휴가 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 물놀이 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 물놀이 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "물놀이 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 해수욕 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 해수욕 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "해수욕 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 겨울 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 겨울 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "겨울 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 연말 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 연말 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "연말 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 크리스마스 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 크리스마스 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "크리스마스 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 신정 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 신정 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "신정 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 설날 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 설날 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "설날 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 추석 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 추석 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "추석 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 봄 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 봄 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "봄 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 벚꽃 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 벚꽃 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "벚꽃 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 가을 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 가을 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "가을 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 단풍 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 단풍 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "단풍 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 일출 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 일출 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "일출 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 낙조 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 낙조 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "낙조 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 불꽃놀이 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 불꽃놀이 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "불꽃놀이 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 성수기 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 성수기 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "성수기 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 비수기 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 비수기 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "비수기 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 주말 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 주말 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "주말 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 평일 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.8 + }, + { + "keyword": "군산 평일 펜션 예약", + "intent": "transactional", + "kind": "keyword", + "category": "시즌", + "relevance": 0.71 + }, + { + "keyword": "평일 군산 숙소", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.68 + }, + { + "keyword": "군산 생일 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 생일 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "군산 기념일 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 기념일 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "군산 결혼기념일 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 결혼기념일 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "군산 프러포즈 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 프러포즈 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "군산 100일 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 100일 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "군산 가족여행 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 가족여행 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "군산 단합대회 펜션", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.75 + }, + { + "keyword": "군산 단합대회 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "시즌", + "relevance": 0.69 + }, + { + "keyword": "선유도해수욕장 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "선유도해수욕장 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "선유도해수욕장 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "선유도해수욕장 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "새만금방조제 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "새만금방조제 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "새만금방조제 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "새만금방조제 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "경암동 철길마을 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "경암동 철길마을 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "경암동 철길마을 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "경암동 철길마을 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "근대역사박물관 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "근대역사박물관 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "근대역사박물관 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "근대역사박물관 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "초원사진관 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "초원사진관 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "초원사진관 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "초원사진관 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "이성당 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "이성당 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "이성당 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "이성당 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "동국사 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "동국사 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "동국사 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "동국사 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "진포해양테마공원 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "진포해양테마공원 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "진포해양테마공원 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "진포해양테마공원 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "신흥동 일본식가옥 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "신흥동 일본식가옥 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "신흥동 일본식가옥 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "신흥동 일본식가옥 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "은파호수공원 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "은파호수공원 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "은파호수공원 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "은파호수공원 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "월명공원 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "월명공원 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "월명공원 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "월명공원 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "금강하구둑 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "금강하구둑 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "금강하구둑 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "금강하구둑 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "철새조망대 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "철새조망대 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "철새조망대 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "철새조망대 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "채만식문학관 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "채만식문학관 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "채만식문학관 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "채만식문학관 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "째보선창 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "째보선창 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "째보선창 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "째보선창 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "시간여행마을 근처 펜션", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.83 + }, + { + "keyword": "시간여행마을 근처 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.8 + }, + { + "keyword": "시간여행마을 펜션 추천", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.72 + }, + { + "keyword": "시간여행마을 가까운 숙소", + "intent": "local", + "kind": "keyword", + "category": "관광지", + "relevance": 0.66 + }, + { + "keyword": "오션뷰", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "바다뷰", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "독채", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "프라이빗", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "스파", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "자쿠지", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "바베큐", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "수영장", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "노을뷰", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "일출뷰", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "월풀", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "온수풀", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "야외수영장", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "인피니티풀", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "불멍", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "화로대", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "넷플릭스", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "빔프로젝터", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "노래방", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "파티룸", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "복층", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "테라스", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "마당", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "벽난로", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "애견운동장", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "키즈룸", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "트램폴린", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "무료주차", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "조식", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "세미나실", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "커플", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "가족", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "친구", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "애견동반", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "단체", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "펜션", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "숙소", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "풀빌라", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "독채펜션", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "스파펜션", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "애견펜션", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "감성펜션", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "글램핑", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "카라반", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "캠핑장", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "게스트하우스", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "민박", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "리조트", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "한옥펜션", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "촌집", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "별장", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "오션뷰객실", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "반려동물동반", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "금연객실", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "엘리베이터", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "와이파이", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "취사가능", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "단체가능", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "조용한", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "신축", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "리모델링", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "뷰맛집", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "인생샷", + "intent": "local", + "kind": "tag", + "category": "태그", + "relevance": 0.6 + }, + { + "keyword": "군산 커플 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 가족 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 친구 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 애견동반 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 단체 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 아이 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 부모님 펜션 어디가 좋아요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산에 오션뷰 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 바다뷰 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 독채 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 프라이빗 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 스파 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 자쿠지 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 바베큐 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "군산에 수영장 펜션 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.67 + }, + { + "keyword": "선유도 펜션 어떻게 가나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.64 + }, + { + "keyword": "선유도 숙소 예약 언제 해야 하나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.6 + }, + { + "keyword": "무녀도 펜션 어떻게 가나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.64 + }, + { + "keyword": "무녀도 숙소 예약 언제 해야 하나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.6 + }, + { + "keyword": "장자도 펜션 어떻게 가나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.64 + }, + { + "keyword": "장자도 숙소 예약 언제 해야 하나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.6 + }, + { + "keyword": "대장도 펜션 어떻게 가나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.64 + }, + { + "keyword": "대장도 숙소 예약 언제 해야 하나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.6 + }, + { + "keyword": "신시도 펜션 어떻게 가나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.64 + }, + { + "keyword": "신시도 숙소 예약 언제 해야 하나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.6 + }, + { + "keyword": "군산 펜션 1박 얼마인가요", + "intent": "transactional", + "kind": "keyword", + "category": "질문형", + "relevance": 0.72 + }, + { + "keyword": "군산 펜션 바베큐 가능한가요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.7 + }, + { + "keyword": "군산 펜션 체크인 몇시인가요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.66 + }, + { + "keyword": "군산 펜션 주차 되나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.66 + }, + { + "keyword": "군산 애견동반 펜션 추가요금 있나요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.63 + }, + { + "keyword": "군산 펜션 성수기 언제인가요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.61 + }, + { + "keyword": "선유도 들어가는 배 시간표", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.55 + }, + { + "keyword": "군산 여행 몇박이 좋을까요", + "intent": "informational", + "kind": "keyword", + "category": "질문형", + "relevance": 0.54 + }, + { + "keyword": "군산 커플 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 커플 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 가족 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 친구 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 애견동반 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "군산 단체 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.5 + }, + { + "keyword": "선유도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "선유도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "선유도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "선유도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "선유도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "무녀도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "무녀도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "무녀도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "무녀도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "무녀도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "장자도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "장자도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "장자도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "장자도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "장자도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "대장도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "대장도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "대장도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "대장도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "대장도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "신시도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "신시도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "신시도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "신시도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "신시도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "야미도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "야미도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "야미도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "야미도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "야미도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "고군산군도 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "고군산군도 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "고군산군도 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "고군산군도 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "고군산군도 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.48 + }, + { + "keyword": "군산 여름휴가 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 여름휴가 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 여름휴가 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 여름휴가 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 여름휴가 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 물놀이 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 물놀이 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 물놀이 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 물놀이 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 물놀이 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 해수욕 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 해수욕 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 해수욕 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 해수욕 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 해수욕 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 겨울 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 겨울 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 겨울 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 겨울 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 겨울 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 연말 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 연말 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 연말 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 연말 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 연말 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 크리스마스 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 크리스마스 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 크리스마스 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 크리스마스 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 크리스마스 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 신정 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 신정 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 신정 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 신정 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 신정 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 설날 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 설날 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 설날 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 설날 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 설날 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 추석 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 추석 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 추석 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 추석 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 추석 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 봄 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 봄 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 봄 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 봄 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 봄 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 벚꽃 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 벚꽃 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 벚꽃 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 벚꽃 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 벚꽃 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 가을 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 가을 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 가을 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 가을 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 가을 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 단풍 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 단풍 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 단풍 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 단풍 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 단풍 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 일출 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 일출 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 일출 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 일출 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 일출 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 낙조 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 낙조 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 낙조 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 낙조 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 낙조 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 불꽃놀이 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 불꽃놀이 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 불꽃놀이 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 불꽃놀이 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 불꽃놀이 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 성수기 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 성수기 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 성수기 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 성수기 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 성수기 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 비수기 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 비수기 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 비수기 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 비수기 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 비수기 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 주말 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 주말 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 주말 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 주말 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 주말 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 평일 커플 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 평일 가족 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 평일 친구 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 평일 애견동반 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 평일 단체 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.44 + }, + { + "keyword": "군산 오션뷰 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 오션뷰 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바다뷰 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 독채 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 프라이빗 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 스파 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 자쿠지 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 바베큐 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 노을뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 일출뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 월풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 온수풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 야외수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 인피니티풀 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 불멍 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 화로대 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 넷플릭스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 빔프로젝터 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 노래방 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 파티룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 복층 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 테라스 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 마당 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 벽난로 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 애견운동장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 키즈룸 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 트램폴린 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 무료주차 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 조식 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "군산 수영장 세미나실 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.4 + }, + { + "keyword": "새만금 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "새만금 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "비응항 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "오식도 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 바베큐 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "은파호수공원 수영장 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "월명동 오션뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "월명동 바다뷰 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "월명동 독채 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "월명동 프라이빗 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "월명동 스파 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + }, + { + "keyword": "월명동 자쿠지 펜션", + "intent": "local", + "kind": "keyword", + "category": "롱테일", + "relevance": 0.42 + } + ] +} diff --git a/docs/architecture.pptx b/docs/architecture.pptx deleted file mode 100644 index 0c3216d..0000000 Binary files a/docs/architecture.pptx and /dev/null differ diff --git a/docs/site-ontology 아키텍쳐.pptx b/docs/site-ontology 아키텍쳐.pptx new file mode 100644 index 0000000..9690470 Binary files /dev/null and b/docs/site-ontology 아키텍쳐.pptx differ diff --git a/drizzle/0001_embedding_384.sql b/drizzle/0001_embedding_384.sql new file mode 100644 index 0000000..39b5dc9 --- /dev/null +++ b/drizzle/0001_embedding_384.sql @@ -0,0 +1,22 @@ +-- 임베딩 모델 전환: 로컬 multilingual-e5-small (384차원) +-- mock/openai 도 384 로 통일한다 (openai 는 dimensions 파라미터로 축소 요청). +DO $$ +BEGIN + IF (SELECT format_type(atttypid, atttypmod) FROM pg_attribute + WHERE attrelid = 'keyword'::regclass AND attname = 'embedding') <> 'vector(384)' THEN + DROP INDEX IF EXISTS keyword_embedding_hnsw; + ALTER TABLE keyword ALTER COLUMN embedding TYPE vector(384) USING NULL::vector(384); + CREATE INDEX keyword_embedding_hnsw ON keyword USING hnsw (embedding vector_cosine_ops); + END IF; + + IF (SELECT format_type(atttypid, atttypmod) FROM pg_attribute + WHERE attrelid = 'qa_pair'::regclass AND attname = 'embedding') <> 'vector(384)' THEN + ALTER TABLE qa_pair ALTER COLUMN embedding TYPE vector(384) USING NULL::vector(384); + END IF; +END $$; + +-- 데이터셋 출처 추적 (수작업 큐레이션 / LLM 생성 구분) +ALTER TABLE keyword ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'llm'; +ALTER TABLE keyword ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'keyword'; +ALTER TABLE keyword ADD COLUMN IF NOT EXISTS category text; +CREATE INDEX IF NOT EXISTS keyword_kind_idx ON keyword (kind, category); diff --git a/package-lock.json b/package-lock.json index efb3eb9..ba5946e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "o2o-site-ontology", "version": "0.1.0", "dependencies": { + "@huggingface/transformers": "^4.2.0", "@nestjs/bullmq": "^11.0.2", "@nestjs/common": "^11.0.12", "@nestjs/core": "^11.0.12", @@ -197,6 +198,16 @@ "node": ">=0.1.90" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -639,6 +650,499 @@ "node": ">=18" } }, + "node_modules/@huggingface/jinja": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz", + "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tokenizers": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", + "license": "Apache-2.0" + }, + "node_modules/@huggingface/transformers": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz", + "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.6", + "@huggingface/tokenizers": "^0.1.3", + "onnxruntime-node": "1.24.3", + "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", + "sharp": "^0.34.5" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -1392,6 +1896,63 @@ "tslib": "^2.1.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -1797,6 +2358,15 @@ "acorn": "^8.14.0" } }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -2020,6 +2590,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -2569,6 +3146,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2601,11 +3212,16 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -2737,6 +3353,12 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -2795,6 +3417,18 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -3003,6 +3637,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", @@ -3252,6 +3892,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3271,6 +3944,12 @@ "dev": true, "license": "ISC" }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3281,6 +3960,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3569,6 +4260,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3672,6 +4369,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -3701,6 +4404,18 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4070,6 +4785,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4107,6 +4831,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.26.0-dev.20260416-b7804b056c", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz", + "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.24.0-dev.20251116-b39e144322", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "license": "MIT" + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -4274,6 +5041,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -4297,6 +5070,29 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4460,6 +5256,23 @@ "dev": true, "license": "ISC" }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -4577,6 +5390,12 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -4603,6 +5422,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -4628,6 +5462,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -4744,6 +5622,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -5081,6 +5965,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -5291,6 +6187,7 @@ "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index af30d52..9322461 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,12 @@ "db:migrate": "tsx src/db/migrate.ts", "db:seed": "tsx src/db/seed.ts", "db:reset": "docker compose down -v && docker compose up -d --wait && npm run db:migrate && npm run db:seed", - "smoke": "tsx scripts/smoke.ts" + "smoke": "tsx scripts/smoke.ts", + "dataset:build": "node scripts/build-dataset.mjs", + "dataset:ingest": "tsx scripts/ingest-dataset.ts" }, "dependencies": { + "@huggingface/transformers": "^4.2.0", "@nestjs/bullmq": "^11.0.2", "@nestjs/common": "^11.0.12", "@nestjs/core": "^11.0.12", diff --git a/public/demo.html b/public/demo.html new file mode 100644 index 0000000..3d6bb30 --- /dev/null +++ b/public/demo.html @@ -0,0 +1,235 @@ + + + + + +키워드 매칭 콘솔 · o2o-site-ontology + + + +
+
+

O2O-SITE-ONTOLOGY

+

키워드 매칭 콘솔

+

업체명이나 문장을 넣으면 적재된 “군산 펜션” 키워드 사전에서 잘 맞는 것을 골라 보여줍니다.

+
연결 확인 중…
+
+ +
+ + + +
+
+ +
+ +
+ +
+
+
+
+ + +
#키워드유사도의도카테고리
매칭 버튼을 눌러 시작하세요
+
+
+
+
+ + + + diff --git a/scripts/build-dataset.mjs b/scripts/build-dataset.mjs new file mode 100644 index 0000000..6fdf0cf --- /dev/null +++ b/scripts/build-dataset.mjs @@ -0,0 +1,176 @@ +/** + * "군산 펜션" SEO/AEO 키워드·태그 데이터셋 1,000건 생성. + * node scripts/build-dataset.mjs → data/gunsan-pension-keywords.json + * + * 어휘는 실제 군산 지명·관광지·숙소 시설 용어로 구성했고, + * 패턴은 한국 로컬 숙박 검색에서 실제로 쓰이는 조합만 전개한다. + * 가치가 높은 순으로 방출하므로 1,000건에서 잘라도 상위 의도가 남는다. + */ +import { writeFileSync, mkdirSync } from 'node:fs'; + +// ──────────────────────────────────────────────── 어휘 (실제 군산 기반) +const REGION = '군산'; + +// 고군산군도·해안 권역 +const ISLANDS = ['선유도', '무녀도', '장자도', '대장도', '신시도', '야미도', '고군산군도']; +// 시내·주요 권역 +const AREAS = ['새만금', '비응항', '오식도', '은파호수공원', '월명동', '나운동', '수송동', '미룡동', '옥도면']; +// 관광지 (근처 숙소 검색의 앵커) +const SPOTS = [ + '선유도해수욕장', '새만금방조제', '경암동 철길마을', '근대역사박물관', '초원사진관', + '이성당', '동국사', '진포해양테마공원', '신흥동 일본식가옥', '은파호수공원', + '월명공원', '금강하구둑', '철새조망대', '채만식문학관', '째보선창', '시간여행마을', +]; + +// 숙소 유형 +const STAY_CORE = ['펜션', '숙소', '풀빌라', '독채펜션', '스파펜션', '애견펜션', '감성펜션']; +const STAY_ALT = ['글램핑', '카라반', '캠핑장', '게스트하우스', '민박', '리조트', '한옥펜션', '촌집', '별장']; + +// 검색 의도어 +const INTENT_CORE = ['추천', '예약', '가격', '후기', '순위']; +const INTENT_MORE = ['저렴한곳', '가성비', '최저가', '실시간예약', '당일예약', '특가', '할인', + '위치', '주차', '전화번호', '체크인시간', '조식포함', '청소상태']; + +// 동반자 +const WITH_CORE = ['커플', '가족', '친구', '애견동반', '단체']; +const WITH_MORE = ['신혼', '아이동반', '유아동반', '부모님', '효도여행', '대학생', 'MT', '워크샵', + '회사', '태교여행', '혼자', '여자끼리', '4인', '6인', '10인', '20인']; + +// 시설·특징 +const FEAT_CORE = ['오션뷰', '바다뷰', '독채', '프라이빗', '스파', '자쿠지', '바베큐', '수영장']; +const FEAT_MORE = ['노을뷰', '일출뷰', '월풀', '온수풀', '야외수영장', '인피니티풀', '불멍', '화로대', + '넷플릭스', '빔프로젝터', '노래방', '파티룸', '복층', '테라스', '마당', '벽난로', + '애견운동장', '키즈룸', '트램폴린', '무료주차', '조식', '세미나실']; + +// 시즌·행사 +const SEASON = ['여름휴가', '물놀이', '해수욕', '겨울', '연말', '크리스마스', '신정', '설날', '추석', + '봄', '벚꽃', '가을', '단풍', '일출', '낙조', '불꽃놀이', '성수기', '비수기', '주말', '평일']; +const OCCASION = ['생일', '기념일', '결혼기념일', '프러포즈', '100일', '가족여행', '워크샵', '단합대회']; + +// ──────────────────────────────────────────────── 방출 +const rows = []; +const seen = new Set(); + +const INTENT_MAP = { + 예약: 'transactional', 실시간예약: 'transactional', 당일예약: 'transactional', + 가격: 'transactional', 최저가: 'transactional', 특가: 'transactional', 할인: 'transactional', + 후기: 'informational', 순위: 'informational', 청소상태: 'informational', 체크인시간: 'informational', +}; +const intentOf = (m) => INTENT_MAP[m] ?? 'local'; + +function add(keyword, { intent = 'local', kind = 'keyword', category, relevance }) { + const k = keyword.replace(/\s+/g, ' ').trim(); + if (!k || seen.has(k)) return false; + seen.add(k); + rows.push({ keyword: k, intent, kind, category, relevance: Math.round(relevance * 100) / 100 }); + return true; +} + +// T1 — 코어: 지역 × 숙소유형 × 의도 +for (const s of STAY_CORE) add(`${REGION} ${s}`, { category: '코어', relevance: 0.97 }); +for (const s of STAY_CORE) for (const m of INTENT_CORE) + add(`${REGION} ${s} ${m}`, { intent: intentOf(m), category: '코어', relevance: 0.93 }); +for (const s of STAY_ALT) add(`${REGION} ${s}`, { category: '코어', relevance: 0.86 }); +for (const m of INTENT_MORE) add(`${REGION} 펜션 ${m}`, { intent: intentOf(m), category: '의도', relevance: 0.88 }); + +// T2 — 섬·권역 × 숙소유형 +for (const g of [ISLANDS, AREAS]) for (const p of g) for (const s of STAY_CORE.slice(0, 4)) + add(`${p} ${s}`, { category: '권역', relevance: g === ISLANDS ? 0.9 : 0.85 }); +for (const p of ISLANDS) for (const m of INTENT_CORE) + add(`${p} 펜션 ${m}`, { intent: intentOf(m), category: '권역', relevance: 0.82 }); + +// T3 — 동반자 +for (const w of WITH_CORE) { + add(`${REGION} ${w} 펜션`, { category: '동반자', relevance: 0.91 }); + for (const m of INTENT_CORE) add(`${REGION} ${w} 펜션 ${m}`, { intent: intentOf(m), category: '동반자', relevance: 0.8 }); + for (const s of STAY_CORE.slice(2, 6)) add(`${REGION} ${w} ${s}`, { category: '동반자', relevance: 0.78 }); +} +for (const w of WITH_MORE) { + add(`${REGION} ${w} 펜션`, { category: '동반자', relevance: 0.82 }); + add(`${REGION} ${w} 펜션 추천`, { category: '동반자', relevance: 0.75 }); + add(`${REGION} ${w} 숙소`, { category: '동반자', relevance: 0.73 }); +} + +// T4 — 시설·특징 +for (const f of FEAT_CORE) { + add(`${REGION} ${f} 펜션`, { category: '시설', relevance: 0.89 }); + add(`${REGION} 펜션 ${f}`, { category: '시설', relevance: 0.76 }); + for (const m of INTENT_CORE.slice(0, 3)) add(`${REGION} ${f} 펜션 ${m}`, { intent: intentOf(m), category: '시설', relevance: 0.72 }); +} +for (const f of FEAT_MORE) { + add(`${REGION} ${f} 펜션`, { category: '시설', relevance: 0.79 }); + add(`${REGION} 펜션 ${f}`, { category: '시설', relevance: 0.7 }); +} +for (const p of ISLANDS.slice(0, 4)) for (const f of FEAT_CORE) + add(`${p} ${f} 펜션`, { category: '시설', relevance: 0.74 }); + +// T5 — 시즌·행사 +for (const s of SEASON) { + add(`${REGION} ${s} 펜션`, { category: '시즌', relevance: 0.8 }); + add(`${REGION} ${s} 펜션 예약`, { intent: 'transactional', category: '시즌', relevance: 0.71 }); + add(`${s} ${REGION} 숙소`, { category: '시즌', relevance: 0.68 }); +} +for (const o of OCCASION) { + add(`${REGION} ${o} 펜션`, { category: '시즌', relevance: 0.75 }); + add(`${REGION} ${o} 펜션 추천`, { category: '시즌', relevance: 0.69 }); +} + +// T6 — 관광지 앵커 +for (const sp of SPOTS) { + add(`${sp} 근처 펜션`, { category: '관광지', relevance: 0.83 }); + add(`${sp} 근처 숙소`, { category: '관광지', relevance: 0.8 }); + add(`${sp} 펜션 추천`, { category: '관광지', relevance: 0.72 }); + add(`${sp} 가까운 숙소`, { category: '관광지', relevance: 0.66 }); +} + +// T7 — 태그 (칩 UI 용 짧은 패싯) +const TAGS = [...FEAT_CORE, ...FEAT_MORE, ...WITH_CORE, ...STAY_CORE, ...STAY_ALT, + '오션뷰객실', '반려동물동반', '금연객실', '엘리베이터', '와이파이', '취사가능', + '단체가능', '조용한', '신축', '리모델링', '뷰맛집', '인생샷']; +for (const t of TAGS) add(t, { kind: 'tag', category: '태그', relevance: 0.6 }); + +// T8 — 질문형 (AEO) +const Q = []; +for (const w of [...WITH_CORE, '아이', '부모님']) Q.push([`${REGION} ${w} 펜션 어디가 좋아요`, 'informational', 0.7]); +for (const f of FEAT_CORE) Q.push([`${REGION}에 ${f} 펜션 있나요`, 'informational', 0.67]); +for (const p of ISLANDS.slice(0, 5)) { + Q.push([`${p} 펜션 어떻게 가나요`, 'informational', 0.64]); + Q.push([`${p} 숙소 예약 언제 해야 하나요`, 'informational', 0.6]); +} +Q.push([`${REGION} 펜션 1박 얼마인가요`, 'transactional', 0.72]); +Q.push([`${REGION} 펜션 바베큐 가능한가요`, 'informational', 0.7]); +Q.push([`${REGION} 펜션 체크인 몇시인가요`, 'informational', 0.66]); +Q.push([`${REGION} 펜션 주차 되나요`, 'informational', 0.66]); +Q.push([`${REGION} 애견동반 펜션 추가요금 있나요`, 'informational', 0.63]); +Q.push([`${REGION} 펜션 성수기 언제인가요`, 'informational', 0.61]); +Q.push([`선유도 들어가는 배 시간표`, 'informational', 0.55]); +Q.push([`${REGION} 여행 몇박이 좋을까요`, 'informational', 0.54]); +for (const [k, i, r] of Q) add(k, { intent: i, category: '질문형', relevance: r }); + +// T9 — 롱테일: 동반자 × 시설 / 권역 × 동반자 / 시즌 × 동반자 +const LONGTAIL = []; +for (const w of WITH_CORE) for (const f of FEAT_CORE) LONGTAIL.push([`${REGION} ${w} ${f} 펜션`, 0.5]); +for (const p of ISLANDS) for (const w of WITH_CORE) LONGTAIL.push([`${p} ${w} 펜션`, 0.48]); +for (const s of SEASON) for (const w of WITH_CORE) LONGTAIL.push([`${REGION} ${s} ${w} 펜션`, 0.44]); +for (const f of FEAT_CORE) for (const f2 of FEAT_MORE) LONGTAIL.push([`${REGION} ${f} ${f2} 펜션`, 0.4]); +for (const a of AREAS) for (const f of FEAT_CORE) LONGTAIL.push([`${a} ${f} 펜션`, 0.42]); +for (const [k, r] of LONGTAIL) { + if (rows.length >= 1000) break; + add(k, { category: '롱테일', relevance: r }); +} + +const dataset = rows.slice(0, 1000); +mkdirSync('data', { recursive: true }); +writeFileSync('data/gunsan-pension-keywords.json', JSON.stringify({ + topic: '군산 펜션', + locale: 'ko-KR', + generatedBy: 'hand-authored vocabulary × search-pattern expansion', + count: dataset.length, + items: dataset, +}, null, 2) + '\n'); + +const by = (f) => dataset.reduce((a, r) => (a[r[f]] = (a[r[f]] ?? 0) + 1, a), {}); +console.log(`✅ data/gunsan-pension-keywords.json ${dataset.length}건`); +console.log(' 카테고리:', by('category')); +console.log(' 의도 :', by('intent')); +console.log(' 종류 :', by('kind')); diff --git a/scripts/ingest-dataset.ts b/scripts/ingest-dataset.ts new file mode 100644 index 0000000..eaefb16 --- /dev/null +++ b/scripts/ingest-dataset.ts @@ -0,0 +1,88 @@ +/** + * data/gunsan-pension-keywords.json 을 pgvector 에 적재한다. + * npx tsx scripts/ingest-dataset.ts + * + * 정책: 주기 수집 없음. 고정 데이터셋 1회 적재. + * 중복제거는 어휘 단계(정규화 완전일치)만 자동 병합하고, + * 벡터 유사도는 자동 병합하지 않고 "검토 목록"으로만 뽑는다. (이유는 README 참조) + */ +import { readFileSync } from 'node:fs'; +import { createSql, toVector } from '../src/db/db'; +import { normalizeKeyword, canonicalizeKeyword, isBanned } from '../src/keywords/normalize'; +import { LocalEmbeddingProvider } from '../src/embedding/local.provider'; +import { MockEmbeddingProvider } from '../src/embedding/mock.provider'; +import { env } from '../src/config/env'; + +type Item = { keyword: string; intent: string; kind: string; category: string; relevance: number }; + +async function main() { + const sql = createSql(); + const embedder = + env.embedding.provider === 'mock' ? new MockEmbeddingProvider() : new LocalEmbeddingProvider(); + + const raw = JSON.parse(readFileSync('data/gunsan-pension-keywords.json', 'utf8')); + const items: Item[] = raw.items; + console.log(`📦 데이터셋 ${items.length}건 · 임베딩 ${embedder.name}`); + + // 1) 파일 내 어휘 중복 정리 + const byNorm = new Map(); + let banned = 0; + for (const it of items) { + const canonical = canonicalizeKeyword(it.keyword); + const norm = normalizeKeyword(it.keyword); + if (!norm || isBanned(canonical)) { banned++; continue; } + const hit = byNorm.get(norm); + if (hit) { + if (!hit.aliases.includes(canonical) && hit.item.keyword !== canonical) hit.aliases.push(canonical); + if (it.relevance > hit.item.relevance) hit.item = it; + } else { + byNorm.set(norm, { item: { ...it, keyword: canonical }, aliases: [] }); + } + } + const uniq = [...byNorm.entries()]; + console.log(` 어휘 중복제거 → ${uniq.length}건 (병합 ${items.length - uniq.length - banned}, 금칙어 ${banned})`); + + // 2) 임베딩 (배치) + const t0 = Date.now(); + const vecs = await embedder.embed(uniq.map(([, v]) => v.item.keyword), 'passage'); + console.log(` 임베딩 ${vecs.length}건 · ${embedder.dimensions}차원 · ${Date.now() - t0}ms`); + + // 3) 적재 + const region = 'kr.jeonbuk.gunsan'; + const industry = 'stay.pension'; + let inserted = 0, updated = 0; + await sql.begin(async (tx) => { + for (let i = 0; i < uniq.length; i++) { + const [norm, v] = uniq[i]; + const res = await tx>` + INSERT INTO keyword + (canonical, normalized, locale, aliases, intent, kind, category, source, + industry_id, region_id, embedding) + VALUES (${v.item.keyword}, ${norm}, 'ko-KR', ${v.aliases}, + ${v.item.intent}::keyword_intent, ${v.item.kind}, ${v.item.category}, 'dataset', + ${industry}, ${region}, ${toVector(vecs[i])}::vector) + ON CONFLICT (normalized, locale) DO UPDATE SET + canonical = EXCLUDED.canonical, aliases = EXCLUDED.aliases, intent = EXCLUDED.intent, + kind = EXCLUDED.kind, category = EXCLUDED.category, source = EXCLUDED.source, + embedding = EXCLUDED.embedding, updated_at = now() + RETURNING (xmax = 0) AS inserted`; + res[0]?.inserted ? inserted++ : updated++; + if (i % 100 === 0) process.stdout.write(`\r 적재 ${i}/${uniq.length}`); + } + }); + console.log(`\r ✅ 신규 ${inserted} · 갱신 ${updated} `); + + // 4) 벡터 근접쌍 — 자동 병합하지 않고 검토 목록으로만 + const near = await sql>` + SELECT k1.canonical AS a, k2.canonical AS b, 1 - (k1.embedding <=> k2.embedding) AS sim + FROM keyword k1 JOIN keyword k2 + ON k1.id < k2.id AND k1.embedding <=> k2.embedding < 0.02 + WHERE k1.source = 'dataset' AND k2.source = 'dataset' + ORDER BY sim DESC LIMIT 15`; + console.log(`\n🔍 벡터 근접쌍 검토 목록 (cos ≥ 0.98, 자동 병합 안 함) — 상위 ${near.length}`); + for (const n of near) console.log(` ${Number(n.sim).toFixed(4)} ${n.a} ↔ ${n.b}`); + + await sql.end(); +} + +main().catch((e) => { console.error('❌', e); process.exit(1); }); diff --git a/src/app.module.ts b/src/app.module.ts index c48aa35..477c904 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,7 @@ import { Controller, Get, Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { env } from './config/env'; import { DbModule } from './db/db.module'; +import { EmbeddingModule } from './embedding/embedding.module'; import { GenerationModule } from './generation/generation.module'; import { MerchantsHttpModule } from './merchants/merchants.controller.module'; import { ServingModule } from './serving/serving.module'; @@ -11,13 +12,19 @@ import { ServingModule } from './serving/serving.module'; class HealthController { @Get('health') health() { - return { status: 'ok', llmProvider: env.llm.provider, ts: new Date().toISOString() }; + return { + status: 'ok', + llmProvider: env.llm.provider, + embeddingProvider: env.embedding.provider, + ts: new Date().toISOString(), + }; } } @Module({ imports: [ DbModule, + EmbeddingModule, ScheduleModule.forRoot(), BullModule.forRoot({ connection: { host: env.redis.host, port: env.redis.port } }), GenerationModule, diff --git a/src/config/env.ts b/src/config/env.ts index 5a8f39b..bd0c19c 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -15,8 +15,12 @@ export const env = { model: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', embeddingModel: process.env.OPENAI_EMBEDDING_MODEL ?? 'text-embedding-3-small', }, + embedding: { + provider: (process.env.EMBEDDING_PROVIDER ?? 'local') as 'mock' | 'local' | 'openai', + localModel: process.env.EMBEDDING_LOCAL_MODEL ?? 'Xenova/multilingual-e5-small', + }, dedup: { - cosineThreshold: num(process.env.DEDUP_COSINE_THRESHOLD, 0.92), + cosineThreshold: num(process.env.DEDUP_COSINE_THRESHOLD, 0.99), trigramThreshold: num(process.env.DEDUP_TRIGRAM_THRESHOLD, 0.6), candidateLimit: num(process.env.DEDUP_CANDIDATE_LIMIT, 20), }, @@ -26,5 +30,5 @@ export const env = { }, } as const; -export const EMBEDDING_DIM = 1536; +export const EMBEDDING_DIM = 384; export const PROMPT_VERSION = 'kw-v1'; diff --git a/src/embedding/embedding.module.ts b/src/embedding/embedding.module.ts new file mode 100644 index 0000000..b24c6f0 --- /dev/null +++ b/src/embedding/embedding.module.ts @@ -0,0 +1,19 @@ +import { Global, Module } from '@nestjs/common'; +import { env } from '../config/env'; +import { LocalEmbeddingProvider } from './local.provider'; +import { MockEmbeddingProvider } from './mock.provider'; +import { OpenAiEmbeddingProvider } from './openai.provider'; +import { EmbeddingProvider } from './types'; + +const IMPL = { + local: LocalEmbeddingProvider, + openai: OpenAiEmbeddingProvider, + mock: MockEmbeddingProvider, +} as const; + +@Global() +@Module({ + providers: [{ provide: EmbeddingProvider, useClass: IMPL[env.embedding.provider] }], + exports: [EmbeddingProvider], +}) +export class EmbeddingModule {} diff --git a/src/embedding/local.provider.ts b/src/embedding/local.provider.ts new file mode 100644 index 0000000..07b1dbb --- /dev/null +++ b/src/embedding/local.provider.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { EMBEDDING_DIM, env } from '../config/env'; +import { EmbedKind, EmbeddingProvider } from './types'; + +/** CommonJS 빌드에서 ESM 전용 패키지를 로드하기 위한 우회 (TS 가 require 로 바꾸지 못하게 한다) */ +const esmImport = new Function('s', 'return import(s)') as (s: string) => Promise; + +/** + * 로컬 multilingual-e5-small (384차원, onnxruntime CPU). + * 최초 1회 모델을 내려받아 캐시하며 그 뒤로는 오프라인 동작한다. + */ +@Injectable() +export class LocalEmbeddingProvider extends EmbeddingProvider { + readonly name = 'local:multilingual-e5-small'; + readonly dimensions = EMBEDDING_DIM; + private readonly logger = new Logger(LocalEmbeddingProvider.name); + private extractor: any | null = null; + private loading: Promise | null = null; + + private async pipe() { + if (this.extractor) return this.extractor; + if (!this.loading) { + this.loading = (async () => { + const t0 = Date.now(); + const { pipeline } = await esmImport('@huggingface/transformers'); + const fe = await pipeline('feature-extraction', env.embedding.localModel); + this.logger.log(`model ready: ${env.embedding.localModel} (${Date.now() - t0}ms)`); + this.extractor = fe; + return fe; + })(); + } + return this.loading; + } + + async embed(texts: string[], kind: EmbedKind = 'passage'): Promise { + if (texts.length === 0) return []; + const fe = await this.pipe(); + const prefixed = texts.map((t) => `${kind}: ${t}`); + const out: number[][] = []; + const BATCH = 64; + for (let i = 0; i < prefixed.length; i += BATCH) { + const slice = prefixed.slice(i, i + BATCH); + const res = await fe(slice, { pooling: 'mean', normalize: true }); + out.push(...(res.tolist() as number[][])); + } + return out; + } +} diff --git a/src/embedding/mock.provider.ts b/src/embedding/mock.provider.ts new file mode 100644 index 0000000..2d77849 --- /dev/null +++ b/src/embedding/mock.provider.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { EMBEDDING_DIM } from '../config/env'; +import { hashEmbedding } from '../llm/mock.provider'; +import { EmbedKind, EmbeddingProvider } from './types'; + +/** 모델 다운로드 없이 파이프라인을 돌리기 위한 문자 bigram 해싱 임베딩. 의미는 잡지 못한다. */ +@Injectable() +export class MockEmbeddingProvider extends EmbeddingProvider { + readonly name = 'mock:bigram-hash'; + readonly dimensions = EMBEDDING_DIM; + async embed(texts: string[], _kind: EmbedKind = 'passage'): Promise { + return texts.map((t) => hashEmbedding(t, EMBEDDING_DIM)); + } +} diff --git a/src/embedding/openai.provider.ts b/src/embedding/openai.provider.ts new file mode 100644 index 0000000..f61d21c --- /dev/null +++ b/src/embedding/openai.provider.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import OpenAI from 'openai'; +import { EMBEDDING_DIM, env } from '../config/env'; +import { EmbedKind, EmbeddingProvider } from './types'; + +@Injectable() +export class OpenAiEmbeddingProvider extends EmbeddingProvider { + readonly name = `openai:${env.llm.embeddingModel}`; + readonly dimensions = EMBEDDING_DIM; + private readonly client = new OpenAI({ apiKey: env.llm.apiKey }); + + async embed(texts: string[], _kind: EmbedKind = 'passage'): Promise { + if (texts.length === 0) return []; + const res = await this.client.embeddings.create({ + model: env.llm.embeddingModel, + input: texts, + dimensions: EMBEDDING_DIM, // 스키마와 차원을 맞춘다 + }); + return res.data.map((d) => d.embedding as number[]); + } +} diff --git a/src/embedding/types.ts b/src/embedding/types.ts new file mode 100644 index 0000000..31dc8b5 --- /dev/null +++ b/src/embedding/types.ts @@ -0,0 +1,8 @@ +/** e5 계열은 query 와 passage 를 비대칭으로 인코딩한다 — 검색 품질에 직접 영향. */ +export type EmbedKind = 'query' | 'passage'; + +export abstract class EmbeddingProvider { + abstract readonly name: string; + abstract readonly dimensions: number; + abstract embed(texts: string[], kind?: EmbedKind): Promise; +} diff --git a/src/generation/generation.service.ts b/src/generation/generation.service.ts index 888f270..b866415 100644 --- a/src/generation/generation.service.ts +++ b/src/generation/generation.service.ts @@ -4,6 +4,7 @@ import { asJson, Sql, toVector } from '../db/db'; import { env, PROMPT_VERSION } from '../config/env'; import { DedupAction, DedupService } from '../keywords/dedup.service'; import { canonicalizeKeyword, normalizeKeyword } from '../keywords/normalize'; +import { EmbeddingProvider } from '../embedding/types'; import { LlmProvider, MerchantContext } from '../llm/types'; import { MerchantsService } from '../merchants/merchants.service'; @@ -35,6 +36,7 @@ export class GenerationService { @Inject(PG) private readonly sql: Sql, private readonly merchants: MerchantsService, private readonly llm: LlmProvider, + private readonly embedder: EmbeddingProvider, private readonly dedup: DedupService, ) {} @@ -72,7 +74,7 @@ export class GenerationService { // 임베딩은 한 번에 배치 호출 (후보 수만큼 왕복하지 않는다) const texts = output.keywords.map((k) => canonicalizeKeyword(k.keyword)); - const embeddings = texts.length ? await this.llm.embed(texts) : []; + const embeddings = texts.length ? await this.embedder.embed(texts, 'passage') : []; const stats: GenerationStats = { runId, @@ -193,7 +195,7 @@ export class GenerationService { pairs: Array<{ question: string; answer: string }>, ): Promise { if (pairs.length === 0) return 0; - const embeddings = await this.llm.embed(pairs.map((p) => p.question)); + const embeddings = await this.embedder.embed(pairs.map((p) => p.question), 'passage'); let created = 0; for (let i = 0; i < pairs.length; i++) { diff --git a/src/keywords/dedup.service.ts b/src/keywords/dedup.service.ts index b6bc8ee..2c2089d 100644 --- a/src/keywords/dedup.service.ts +++ b/src/keywords/dedup.service.ts @@ -61,6 +61,12 @@ export class DedupService { } // 2~3단계 — trigram 후보 + 벡터 ANN 후보를 모아 최고 유사도 판정 + // + // 주의: 짧은 한글 키워드에서는 문장 임베딩의 절대 코사인이 변별력이 약하다. + // 실측(multilingual-e5-small): '선유도 펜션' ↔ '새만금 펜션' = 0.936, + // '군산 펜션' ↔ '군산 호텔' = 0.970 — 전혀 다른 키워드인데도 높게 나온다. + // 반면 어순만 바뀐 진짜 중복('군산 키즈룸 펜션' ↔ '군산 펜션 키즈룸')은 0.999 대에 몰린다. + // 그래서 임계값을 0.99 로 올려 잡고, 자동 병합의 주력은 1~2단계(어휘)에 둔다. const candidates = await this.repo.findDedupCandidates( input.embedding, normalized, diff --git a/src/llm/mock.provider.ts b/src/llm/mock.provider.ts index 40b029b..7d28e24 100644 --- a/src/llm/mock.provider.ts +++ b/src/llm/mock.provider.ts @@ -22,9 +22,6 @@ export class MockLlmProvider extends LlmProvider { readonly name = 'mock'; readonly model = 'mock-keyword-v1'; - async embed(texts: string[]): Promise { - return texts.map((t) => hashEmbedding(t)); - } async generate(ctx: MerchantContext): Promise { const region = ctx.regionName ?? ''; diff --git a/src/llm/openai.provider.ts b/src/llm/openai.provider.ts index ba35213..1b90db7 100644 --- a/src/llm/openai.provider.ts +++ b/src/llm/openai.provider.ts @@ -48,14 +48,6 @@ export class OpenAiLlmProvider extends LlmProvider { private readonly logger = new Logger(OpenAiLlmProvider.name); private readonly client = new OpenAI({ apiKey: env.llm.apiKey }); - async embed(texts: string[]): Promise { - if (texts.length === 0) return []; - const res = await this.client.embeddings.create({ - model: env.llm.embeddingModel, - input: texts, - }); - return res.data.map((d) => d.embedding as number[]); - } async generate(ctx: MerchantContext): Promise { const res = await this.client.chat.completions.create({ diff --git a/src/llm/types.ts b/src/llm/types.ts index 3f00f81..e1c9db6 100644 --- a/src/llm/types.ts +++ b/src/llm/types.ts @@ -43,5 +43,4 @@ export abstract class LlmProvider { abstract readonly name: string; abstract readonly model: string; abstract generate(ctx: MerchantContext): Promise; - abstract embed(texts: string[]): Promise; } diff --git a/src/serving/demo.controller.ts b/src/serving/demo.controller.ts new file mode 100644 index 0000000..52ea5aa --- /dev/null +++ b/src/serving/demo.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Header } from '@nestjs/common'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +/** 로컬 확인용 매칭 데모 페이지. 빌드 산출물이 아니라 public/ 에서 직접 읽는다. */ +@Controller() +export class DemoController { + @Get('demo') + @Header('content-type', 'text/html; charset=utf-8') + demo(): string { + return readFileSync(join(process.cwd(), 'public', 'demo.html'), 'utf8'); + } +} diff --git a/src/serving/serving.controller.ts b/src/serving/serving.controller.ts index 533f748..c41dbf4 100644 --- a/src/serving/serving.controller.ts +++ b/src/serving/serving.controller.ts @@ -23,6 +23,12 @@ export class ServingController { return this.serving.searchKeywords(body.query, Math.min(body.limit ?? 10, 50)); } + /** 자유 입력(업체명/문장) → 적재된 사전에서 잘 맞는 키워드 */ + @Post('match') + match(@Body() body: { query: string; limit?: number }) { + return this.serving.match(body.query ?? '', Math.min(body.limit ?? 40, 200)); + } + /** 성과 피드백 주입 (Search Console / 유입 로그) */ @Post('sites/:id/performance') performance( diff --git a/src/serving/serving.module.ts b/src/serving/serving.module.ts index 3d5eb89..7e637fa 100644 --- a/src/serving/serving.module.ts +++ b/src/serving/serving.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { KeywordsModule } from '../keywords/keywords.module'; -import { LlmModule } from '../llm/llm.module'; import { MerchantsModule } from '../merchants/merchants.module'; +import { DemoController } from './demo.controller'; import { ServingController } from './serving.controller'; import { ServingService } from './serving.service'; @Module({ - imports: [MerchantsModule, KeywordsModule, LlmModule], - controllers: [ServingController], + imports: [MerchantsModule, KeywordsModule], + controllers: [ServingController, DemoController], providers: [ServingService], }) export class ServingModule {} diff --git a/src/serving/serving.service.ts b/src/serving/serving.service.ts index cedba96..d6fd9be 100644 --- a/src/serving/serving.service.ts +++ b/src/serving/serving.service.ts @@ -1,9 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { PG } from '../db/db.module'; -import { Sql } from '../db/db'; +import { Sql, toVector } from '../db/db'; import { KeywordRepository } from '../keywords/keyword.repository'; -import { LlmProvider } from '../llm/types'; -import { MerchantsService } from '../merchants/merchants.service'; +import { normalizeKeyword } from '../keywords/normalize'; +import { EmbeddingProvider } from '../embedding/types'; +import { MerchantsService, MerchantWithTaxonomy } from '../merchants/merchants.service'; export interface SeoPayload { merchant: { id: string; externalId: string; name: string; siteUrl: string | null }; @@ -34,7 +35,7 @@ export class ServingService { @Inject(PG) private readonly sql: Sql, private readonly merchants: MerchantsService, private readonly keywords: KeywordRepository, - private readonly llm: LlmProvider, + private readonly embedder: EmbeddingProvider, ) {} async seo(idOrExternalId: string, limit: number): Promise { @@ -95,8 +96,88 @@ export class ServingService { }; } + /** + * 자유 입력(업체명 또는 문장) → 적재된 키워드 사전에서 잘 맞는 것을 골라준다. + * 업체명이면 먼저 업체를 해석해 프로필 전체를 질의문으로 쓴다 — + * 상호만으로 임베딩하면 브랜드명 하나로 검색하는 것과 같아 매칭이 얕아진다. + */ + async match(rawQuery: string, limit: number) { + const query = rawQuery.trim(); + const merchant = await this.resolveMerchant(query); + + const queryText = merchant + ? [ + merchant.name, + merchant.region_name, + merchant.industry_name, + merchant.description, + ...toStrings(merchant.profile?.['services']), + ...toStrings(merchant.profile?.['features']), + ...toStrings(merchant.profile?.['audiences']), + ...toStrings(merchant.profile?.['nearby']), + ].filter(Boolean).join(' ') + : query; + + const [embedding] = await this.embedder.embed([queryText], 'query'); + const rows = await this.sql>` + SELECT k.id, k.canonical, k.intent, k.kind, k.category, k.aliases, k.usage_count, + 1 - (k.embedding <=> ${toVector(embedding)}::vector) AS score, + (mk.merchant_id IS NOT NULL) AS linked + FROM keyword k + LEFT JOIN merchant_keyword mk + ON mk.keyword_id = k.id AND mk.merchant_id = ${merchant?.id ?? null} + WHERE k.embedding IS NOT NULL + ORDER BY k.embedding <=> ${toVector(embedding)}::vector + LIMIT ${limit}`; + + return { + input: query, + resolved: merchant + ? { + id: merchant.id, externalId: merchant.external_id, name: merchant.name, + region: merchant.region_name, industry: merchant.industry_name, + description: merchant.description, siteUrl: merchant.site_url, + profile: merchant.profile ?? {}, + } + : null, + queryText, + embeddingProvider: this.embedder.name, + total: await this.dictionarySize(), + matches: rows.map((r) => ({ ...r, score: Number(r.score) })), + }; + } + + /** 상호는 띄어쓰기가 흔들린다 — 정규화 일치 → trigram 순으로 해석 */ + private async resolveMerchant(query: string) { + const norm = normalizeKeyword(query); + if (!norm) return null; + const rows = await this.sql>` + SELECT m.*, + i.name AS industry_name, i.path::text AS industry_path, + r.name AS region_name, r.path::text AS region_path, + similarity(regexp_replace(lower(m.name), '\\s', '', 'g'), ${norm}) AS sim + FROM merchant m + LEFT JOIN industry i ON i.id = m.industry_id + LEFT JOIN region r ON r.id = m.region_id + WHERE regexp_replace(lower(m.name), '\\s', '', 'g') = ${norm} + OR m.external_id = ${query} + OR similarity(regexp_replace(lower(m.name), '\\s', '', 'g'), ${norm}) >= 0.45 + ORDER BY sim DESC NULLS LAST + LIMIT 1`; + return rows[0] ?? null; + } + + async dictionarySize() { + const [row] = await this.sql>` + SELECT count(*)::int AS n FROM keyword WHERE embedding IS NOT NULL`; + return row?.n ?? 0; + } + async searchKeywords(query: string, limit: number) { - const [embedding] = await this.llm.embed([query]); + const [embedding] = await this.embedder.embed([query], 'query'); return this.keywords.searchByVector(embedding, 'ko-KR', limit); } @@ -136,6 +217,10 @@ export class ServingService { } } +function toStrings(v: unknown): string[] { + return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []; +} + function buildDescription(name: string, desc: string, keywords: string[]): string { const base = desc?.trim() || `${name} 안내`; const tail = keywords.slice(0, 3).join(', ');