Best Keyword - 전체 구조 문서
DB 필드 설명
seed_results.db에 저장되는 6개 테이블의 각 필드가 의미하는 바를 정리.
공통 개념
파이프라인은 원시 키워드를 수집한 뒤 여러 단계를 거쳐 점수화한다. 각 단계에서 생성된 데이터가 서로 다른 테이블에 저장된다.
수집된 키워드 → [clustered] → [scored] → [seed_report]
정제/군집화 점수화 최종 리포트
추가로 연속 확장을 위한 영구 테이블 3개가 매 실행마다 누적된다:
[keyword_snapshots] [discovered_domains] [expansion_queue]
수집 이력 누적 자동 발견 도메인 키워드 확장 대기열
1. clustered 테이블
정제 + 군집화까지 완료된 키워드 후보 목록. 동일한 의미의 키워드가 여러 표현으로 수집되면 하나의 클러스터로 묶이고, 가장 적절한 표현이 대표(representative)로 선정된다.
| 필드 | 타입 | 설명 |
|------|------|------|
| phrase | TEXT | 형태소 분석 또는 정규식으로 추출된 원시 후보 표현 |
| normalized | TEXT | phrase를 정규화한 값 (소문자, 특수문자 제거, 공백 정리) |
| source | TEXT | 이 키워드가 수집된 출처 |
| platform | TEXT | 수집 플랫폼 (naver, google, internal 등) |
| created_at | TIMESTAMP | 수집 시각 |
| base_text | TEXT | 이 후보가 추출된 원본 문장 전체 |
| token_count | INTEGER | 공백으로 분리한 토큰 수 (예: "스킨 케어" → 2) |
| char_len | INTEGER | 정규화된 텍스트의 글자 수 |
| cluster_id | INTEGER | 군집화 후 할당된 클러스터 번호. 같은 번호면 유사 표현 |
| representative | TEXT | 해당 클러스터에서 선정된 대표 키워드. 이후 점수화의 기준이 됨 |
source 값 목록:
| 값 | 의미 |
|----|------|
| naver_datalab_topic | 네이버 쇼핑인사이트, 데이터랩, 뉴스 인기기사, 연관검색어 |
| google_trends_topic | Google Trends suggestions, Google Trends RSS |
| autocomplete | 네이버 자동완성 API, 범용 자동완성 API |
| internal_search | 내부 검색 로그 |
| product_catalog | 상품 카탈로그 텍스트 |
| review | 리뷰 텍스트 |
| faq | FAQ 텍스트 |
| community_title | 커뮤니티 게시글 제목 |
| blog_title | 블로그 제목 |
| news_title | 뉴스 제목 |
참고: naver_datalab_topic은 meta.type으로 세부 출처를 구분:
shopping_keyword: 쇼핑인사이트 카테고리별 인기 키워드news_trending: 네이버 뉴스 인기기사 제목related_search: 네이버 연관검색어
2. scored 테이블
대표 키워드(representative)별로 종합 점수를 계산한 결과.
clustered 테이블의 데이터를 대표 키워드 기준으로 집계(aggregation)하여 생성된다.
기본 정보
| 필드 | 타입 | 설명 |
|------|------|------|
| representative | TEXT | 대표 키워드. clustered의 같은 이름 필드와 연결됨 |
| raw_frequency | INTEGER | 이 대표 키워드가 속한 클러스터 내 정규화된 표현의 총 등장 횟수 |
| source_diversity | INTEGER | 이 키워드가 등장한 서로 다른 source의 수. 여러 출처에서 보이면 높음 |
| platform_diversity | INTEGER | 이 키워드가 등장한 서로 다른 platform의 수 |
| latest_seen | TIMESTAMP | 이 키워드가 마지막으로 관측된 시각 |
| sample_phrase | TEXT | 이 클러스터에서 처음 등장한 정규화된 표현 (참고용) |
원시 점수 (Raw Scores)
계산 직후 정규화되기 전의 값들. 상대적 크기 비교에 사용.
| 필드 | 타입 | 단위 | 설명 |
|------|------|------|------|
| weighted_frequency | REAL | 가중합 | raw_frequency에 소스별 가중치를 곱한 합계. 높을수록 자주, 신뢰할 수 있는 출처에서 등장 |
| growth_raw | REAL | 로그스케일 | 최근 7일 vs 이전 기간 대비 증가율. 새로 등장한 키워드는 2 + log(등장횟수), 기존 키워드는 (최근-이전)/이전 |
| expandability_raw | REAL | 0~ | 이 키워드가 코퍼스 내에서 하위 키워드를 얼마나 많이 품고 있는지. 자식 키워드 수 × 0.65 + 의도어 결합 가능성 × 0.35. 1~2토큰 키워드에 보너스 |
| business_raw | REAL | 0.4~1.0 | 비즈니스 관련성. 키워드 내 비즈니스 사전어(추천, 가격, 비교, 후기, 구매, 효능, 사용법, 브랜드, 순위, 가성비) 포함 여부에 따라 0.4 + n×0.2 |
정규화 점수 (Normalized Scores)
원시 점수를 0~100 스케일로 변환 (min-max scaling).
최종 seed_score 계산에 직접 사용됨.
| 필드 | 타입 | 범위 | 설명 |
|------|------|------|------|
| frequency_norm | REAL | 0~100 | weighted_frequency의 상대적 순위 |
| source_diversity_norm | REAL | 0~100 | source_diversity의 상대적 순위 |
| growth_norm | REAL | 0~100 | growth_raw의 상대적 순위 |
| expandability_norm | REAL | 0~100 | expandability_raw의 상대적 순위 |
| business_norm | REAL | 40~100 | business_raw × 100 |
최종 결과
| 필드 | 타입 | 범위 | 설명 |
|------|------|------|------|
| seed_score | REAL | 0~100 | 종합 점수. 가중 합산 공식으로 계산 (아래 공식 참조) |
| is_promoted_seed | INTEGER | 0 또는 1 | seed_score ≥ 65인 경우 1 (승격된 시드). 그 외 0 |
| intent | TEXT | 분류값 | 검색 의도 분류 |
| trend_type | TEXT | 분류값 | 시간 트렌드 분류 |
| domain | TEXT | 문자열 | 자동 발견된 도메인명 (DomainDiscoverer 결과) |
seed_score 공식:
seed_score =
source_diversity_norm × 0.30
+ frequency_norm × 0.25
+ growth_norm × 0.20
+ expandability_norm × 0.15
+ business_norm × 0.10
intent 분류값:
| 값 | 의미 | 판정 기준 |
|----|------|-----------|
| 구매형 | 구매 의도 | 추천, 가격, 할인, 순위, 브랜드, 가성비, 구매 포함 |
| 정보형 | 정보 탐색 | 뜻, 방법, 효능, 원인, 정의, 비교, 차이 포함 |
| 문제해결형 | 문제 해결 | 안됨, 오류, 해결, 고장, 증상, 안되는 포함 |
| 속성형 | 속성 한정 | 저분자, 무선, 고단백, 민감성, 여성, 연령대 등 포함 |
| 토픽허브형 | 일반 토픽 | 위 어느 것도 해당하지 않음. 확장 가능한 중심 주제 |
trend_type 분류값:
| 값 | 의미 | 판정 기준 |
|----|------|-----------|
| 신규급등 | 새롭게 급등한 키워드 | 최근 7일에만 등장. 이전에는 없었음 |
| 급상승 | 빠르게 상승 중 | 최근 7일/30일에 등장, 직전 달에는 없었음 |
| 지속상승 | 꾸준히 상승 중 | 직전 달에도 있었고, 최근 순위가 10위 이상 상승 |
| 안정 | 꾸준히 유지 | 모든 기간에 등장, 순위 변동 크지 않음 |
| 보합 | 중간 상태 | 최근 30일에는 있으나 최근 7일에는 없음 |
| 하락 | 감소 추세 | 직전 달에만 있었고 최근에는 사라짐 |
3. seed_report 테이블
최종 리포트. 상위 50개 키워드만 저장.
scored 테이블에 클러스터 멤버 정보와 출처 정보를 결합한 형태.
| 필드 | 타입 | 설명 |
|------|------|------|
| representative | TEXT | 대표 키워드 |
| intent | TEXT | 검색 의도 분류 |
| trend_type | TEXT | 시간 트렌드 분류 |
| seed_score | REAL | 종합 점수 (0~100) |
| is_promoted_seed | INTEGER | 시드 승격 여부 (0=미승격, 1=승격) |
| weighted_frequency | REAL | 가중 출현 빈도 |
| source_diversity | INTEGER | 출처 다양성 (서로 다른 source 수) |
| platform_diversity | INTEGER | 플랫폼 다양성 |
| growth_raw | REAL | 성장률 원시값 |
| expandability_raw | REAL | 확장성 원시값 |
| cluster_members | TEXT | 동일 클러스터에 속한 다른 표현들 (최대 8개, 쉼표 구분) |
| sources | TEXT | 이 키워드가 수집된 출처 목록 (쉼표 구분) |
| latest_seen | TIMESTAMP | 마지막 관측 시각 |
4. keyword_snapshots 테이블 (영구 누적)
매 실행마다 수집된 키워드를 누적 저장. 동일 키워드가 여러 실행에서 관측되면 여러 행이 존재함.
| 필드 | 타입 | 설명 |
|------|------|------|
| id | INTEGER PK | 자동 증가 ID |
| keyword | TEXT | 수집된 키워드 |
| source | TEXT | 수집 출처 |
| domain | TEXT | 자동 발견된 도메인명 (초기 NULL, 도메인 발견 후 업데이트) |
| rank | INTEGER | 해당 카테고리/기간에서의 순위 |
| period | TEXT | 시간 기간 (recent_7d, recent_30d, prev_month 등) |
| category | TEXT | 쇼핑 카테고리명 (화장품/미용, 식품 등) |
| collected_at | TIMESTAMP | 수집 시각 |
인덱스: keyword, domain
5. discovered_domains 테이블 (영구 누적, UPSERT)
DomainDiscoverer가 자동으로 발견한 도메인(세부 카테고리).
| 필드 | 타입 | 설명 |
|------|------|------|
| domain_name | TEXT PK | 발견된 도메인명 (핵심 키워드 기반 자동 생성) |
| core_keywords | TEXT | 도메인을 구성하는 핵심 키워드 목록 (쉼표 구분) |
| keyword_count | INTEGER | 도메인에 속한 키워드 수 |
| discovered_at | TIMESTAMP | 최초 발견 시각 |
| last_expanded_at | TIMESTAMP | 마지막 업데이트 시각 |
6. expansion_queue 테이블 (영구 누적)
키워드 확장 대기열. 매 실행 시 pending 상태의 키워드를 꺼내서 자동완성/연관검색어로 확장한 뒤 done으로 표시.
| 필드 | 타입 | 설명 |
|------|------|------|
| id | INTEGER PK | 자동 증가 ID |
| keyword | TEXT | 확장 대상 키워드 |
| source | TEXT | 이 키워드가 발견된 출처 |
| status | TEXT | pending 또는 done |
| created_at | TIMESTAMP | 큐 등록 시각 |
인덱스: status
동작 흐름:
- 매 실행 시 새로 발견된 키워드 →
status='pending'으로 INSERT get_pending_keywords(limit=30)→ pending 상태 키워드 최대 30개 조회- 자동완성 + 연관검색어로 확장 → 새 키워드 발견
mark_queue_done(keywords)→status='done'으로 업데이트- 새로 발견된 키워드는 다시 pending으로 등록 (다음 실행 시 확장)
테이블 간 관계
clustered scored seed_report
┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ phrase │ │ representative (PK) │◄───│ representative │
│ normalized │ │ raw_frequency │ │ intent │
│ source │──┐ │ source_diversity │ │ trend_type │
│ platform │ │ │ ... │ │ seed_score │
│ created_at │ │ │ seed_score │ │ is_promoted_seed │
│ base_text │ │ │ is_promoted_seed │ │ ... │
│ token_count │ │ │ intent │ │ cluster_members │
│ char_len │ │ │ trend_type │ │ sources │
│ cluster_id │ └─│ representative 기준 │ └──────────────────────┘
│ representative ──│────│ 으로 groupby 집계 │
└──────────────────┘ └─────────────────────┘
keyword_snapshots discovered_domains expansion_queue
┌──────────────────┐ ┌───────────────────────┐ ┌──────────────────┐
│ keyword │◄───│ domain_name (PK) │ │ keyword │
│ source │ │ core_keywords │ │ source │
│ domain ──────────│───►│ keyword_count │ │ status │
│ rank │ │ discovered_at │ │ created_at │
│ period │ │ last_expanded_at │ └──────────────────┘
│ category │ └───────────────────────┘
│ collected_at │
└──────────────────┘
- clustered → scored: 같은
representative값을 가진 행들을 groupby하여 집계 - scored → seed_report: scored에 클러스터 멤버/출처 정보를 조인하여 상위 50개만 추출
- keyword_snapshots ↔ discovered_domains: snapshots의
domain이 domains의domain_name을 참조 - expansion_queue → keyword_snapshots: 큐에서 확장된 키워드가 새 snapshots에 기록됨
프로젝트 개요
네이버/구글에서 키워드를 자동 수집하여, 끝없이 연관 키워드와 도메인을 발견하는 연속 파이프라인. 매 실행마다 이전에 발견한 키워드에서 확장하여 새 키워드와 도메인을 계속 찾아냄. 수동 키워드 입력 없이 전체 과정이 자동으로 동작.
실행
uv run python main.py
출력
- 터미널: AUTO SEED REPORT (순위표) + 도메인 발견 결과 + 확장 큐 상태
- SQLite:
seed_results.db- 매 실행 덮어쓰기:
clustered,scored,seed_report - 영구 누적:
keyword_snapshots,discovered_domains,expansion_queue
- 매 실행 덮어쓰기:
전체 파이프라인 흐름
┌─────────────────────────────────────────────────────────────────┐
│ A. 트렌드 키워드 수집 (Collectors) │
│ │
│ 네이버 쇼핑인사이트 (9카테고리 × 3기간 × 2페이지) ──┐ │
│ Google Trends suggestions (pytrends) ──┤ │
│ Google Trends RSS (실시간 급상승) ──┤──→ TextRecord[]│
│ 네이버 뉴스 인기기사 (실시간) ──┘ │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ B. 확장 큐 처리 (Expansion Queue) │
│ │
│ 이전 실행에서 pending 상태로 남은 키워드 꺼냄 │
│ → 네이버 자동완성 수집 │
│ → 네이버 연관검색어 수집 │
│ → done 표시 │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ C. 상위 키워드 확장 (Top Keyword Expansion) │
│ │
│ 이번에 수집한 상위 키워드에 대해 │
│ → 네이버 자동완성 수집 │
│ → 네이버 연관검색어 수집 │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ D. 도메인 자동 발견 (Domain Discovery) │
│ │
│ 수집된 전체 데이터에서 공동출현 네트워크 구성 │
│ → BFS 연결요소 탐색 → 도메인(세부 카테고리) 자동 추출 │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ 후보 생성 (Generator) │
│ │
│ TextRecord → 형태소 분석/정규식 → 후보 phrase 추출 │
│ kiwipiepy 있으면 형태소 분석, 없으면 정규식 fallback │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ 정제 (Cleaner) │
│ │
│ 불용어 제거 / 길이 필터 / 숫자만 제거 / URL 제거 │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ 군집화 (Clusterer) │
│ │
│ 유사 표현을 하나로 묶고 대표 키워드 선정 │
│ - 포함관계 (a in b) │
│ - Fuzzy matching (rapidfuzz ≥ 90) │
│ - Cosine similarity (TF-IDF ≥ 0.82) │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ 점수화 (Scorer) │
│ │
│ source_diversity × 0.30 │
│ frequency × 0.25 │
│ growth × 0.20 │
│ expandability × 0.15 │
│ business_relevance × 0.10 │
│ ───── │
│ seed_score (0~100) │
│ is_promoted_seed ≥ 65 │
└────────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────┐
│ 분류 + 리포트 (Classifiers) │
│ │
│ intent: 구매형 / 정보형 / 문제해결형 / 속성형 / │
│ 토픽허브형 │
│ trend_type: 신규급등 / 급상승 / 지속상승 / 안정 / │
│ 보합 / 하락 │
└────────────────────────────────┬────────────────────────────────┘
│
▼
E. 영구 저장 + 다음 실행 준비
├── keyword_snapshots: 누적 키워드 저장
├── discovered_domains: 발견된 도메인 UPSERT
├── expansion_queue: 새 키워드 pending 추가
├── clustered / scored / seed_report: 이번 실행 결과 덮어쓰기
└── 터미널 출력 (리포트 + 통계)
파일 구성
best_keyword/
├── main.py # 전체 파이프라인 (단일 파일)
├── pyproject.toml # 의존성 관리 (uv)
├── .env # 환경변수 (API 키 등)
├── seed_results.db # 출력 SQLite (실행 후 생성)
└── docs/
├── architecture.md # 자동 시드 개념 설계 문서
├── structure.md # 이 파일 - 구조 문서
└── db_fields.md # DB 필드 설명
데이터 모델
TextRecord
수집된 원시 텍스트 레코드.
| 필드 | 타입 | 설명 |
|------|------|------|
| text | str | 원문 |
| source | str | 출처 (naver_datalab_topic, google_trends_topic, autocomplete 등) |
| created_at | datetime | 수집 시각 |
| platform | str | 플랫폼 (naver, google, internal) |
| meta | dict | 부가 정보 (카테고리, 기간, 순위, seed 키워드 등) |
KeywordTrendPoint
시계열 트렌드 데이터 포인트.
| 필드 | 타입 | 설명 |
|------|------|------|
| keyword | str | 키워드 |
| date | str | 날짜 (YYYY-MM-DD) |
| value | float | 트렌드 값 |
| source | str | 출처 |
| platform | str | 플랫폼 |
TopicGroup
네이버 DataLab 조회용 키워드 그룹.
| 필드 | 타입 | 설명 |
|------|------|------|
| group_name | str | 그룹명 |
| keywords | list[str] | 그룹에 포함된 키워드 목록 |
DomainCluster
도메인 자동 발견 결과.
| 필드 | 타입 | 설명 |
|------|------|------|
| domain_name | str | 발견된 도메인명 (핵심 키워드 기반) |
| core_keywords | list[str] | 도메인을 구성하는 핵심 키워드 |
| member_count | int | 도메인에 속한 키워드 수 |
수집기 (Collectors)
1. NaverShoppingInsightCollector
네이버 쇼핑인사이트에서 카테고리별 인기 검색어 자동 수집. API 키 불필요. 내부 엔드포인트 직접 호출.
수집 범위:
- 9개 대분류 카테고리 전체
- 3개 시간 기간 (최근 7일 / 최근 30일 / 직전 달)
- 카테고리당 2페이지 (40개 키워드)
- 총 9 × 3 × 40 = 최대 1,080개 키워드
카테고리 목록:
| 코드 | 카테고리 | |------|----------| | 50000000 | 패션의류 | | 50000001 | 패션잡화 | | 50000002 | 화장품/미용 | | 50000003 | 디지털/가전 | | 50000004 | 가구/인테리어 | | 50000005 | 출산/육아 | | 50000006 | 식품 | | 50000007 | 스포츠/레저 | | 50000008 | 생활/건강 |
시간 기간 구조:
| 기간 라벨 | 범위 | timeUnit | 용도 |
|-----------|------|----------|------|
| recent_7d | 최근 7일 | date | 단기 급등 감지 |
| recent_30d | 최근 30일 | month | 중기 트렌드 |
| prev_month | 직전 달 전체 | month | 비교 기준선 |
엔드포인트:
POST https://datalab.naver.com/shoppingInsight/getCategoryKeywordRank.naver
meta 구조:
{
"type": "shopping_keyword",
"category": "화장품/미용",
"period": "recent_7d",
"rank": 1,
"page": 1,
}
2. GoogleTrendingRSSCollector
Google Trends RSS 피드에서 실시간 급상승 검색어 수집. API 키 불필요.
엔드포인트:
GET https://trends.google.com/trending/rss?geo=KR
특징:
- RSS/Atom 형식 모두 파싱 지원
- 약 10~20개 일일 급상승 검색어 반환
- 트래픽 수치, 관련 뉴스, 발행 시간 포함
- source="google_trends_topic", meta={"provider": "rss"}
3. NaverNewsTrendingCollector
네이버 뉴스 인기기사 제목에서 트렌드 키워드 추출. API 키 불필요. HTML 파싱.
엔드포인트:
GET https://news.naver.com/main/ranking/popularMemo.naver
특징:
- 약 250~370개 인기 뉴스 제목 추출
class="list_title"파싱으로 제목 추출- source="naver_datalab_topic", meta={"type": "news_trending"}
4. PyTrendsCollector
Google Trends에서 관련 키워드 수집.
pytrends 라이브러리 사용. suggestions() 메서드만 안정적으로 동작.
동작:
- seed_terms 목록의 각 단어에 대해
suggestions()호출 - 각 seed당 약 4~5개 관련 키워드 반환
- 429 방지를 위해 요청 간 0.5초 대기
기본 seed_terms: 스킨케어, 건강, 다이어트, 운동, 뷰티, 식품, 영양
5. NaverAutocompleteCollector
네이버 자동완성 API에서 연관 키워드 수집. API 키 불필요.
엔드포인트:
GET https://ac.search.naver.com/nx/ac?st=10&r_format=json&q={키워드}
특징:
- 키워드당 약 8개 자동완성 결과 반환
- 요청 간 0.3초 대기
- source="autocomplete", meta={"type": "naver_autocomplete", "seed": seed}
- 확장 큐 처리와 상위 키워드 확장에 사용
6. NaverRelatedSearchCollector
네이버 검색 결과의 연관검색어 수집. API 키 불필요. HTML 파싱.
엔드포인트:
GET https://search.naver.com/search.naver?query={키워드}
특징:
<div class="related_srch">에서 연관검색어 추출- 요청 간 0.5초 대기
- source="naver_datalab_topic", meta={"type": "related_search", "seed": seed}
- 확장 큐 처리와 상위 키워드 확장에 사용
7. NaverDataLabDirectCollector
네이버 데이터랩 검색어 트렌드 직접 수집. API 키 불필요. 웹페이지 내부 흐름을 모방.
동작 원리:
POST /qcHash.naver→hashKey발급GET /keyword/trendResult.naver?hashKey=...→ 결과 HTML- HTML에서
<div id="graph_data">내 JSON 추출
용도: 쇼핑인사이트에서 발견한 상위 키워드의 트렌드 검증 (시계열 데이터).
8. GenericJSONAutocompleteCollector (선택)
자동완성 API 범용 수집기.
특정 엔드포인트를 하드코딩하지 않고 .env 설정으로 주입.
활성화 조건: .env에서 AUTOCOMPLETE_ENABLED=true 설정 시에만 동작.
연속 확장 시스템
파이프라인은 단발성이 아니라 매 실행마다 누적되는 구조.
확장 큐 (expansion_queue)
실행 1: 트렌드 수집 → 1,000개 키워드 발견 → 500개 pending 등록
실행 2: 500개 pending → 자동완성/연관검색어 확장 → 300개 새 키워드 → 200개 pending 등록
실행 3: 200개 pending → 확장 → 150개 새 키워드 → ...
- 매 실행 시 pending 상태의 키워드를 최대 30개 꺼내서 확장
- 확장 완료 후 done 표시
- 새로 발견된 키워드는 다시 pending으로 등록
- 큐에 이미 있는 키워드는 중복 등록하지 않음
도메인 자동 발견 (DomainDiscoverer)
수집된 키워드 간 공동출현 네트워크를 구성하여 자동으로 세부 카테고리를 발견.
알고리즘:
- 자동완성/연관검색어 결과에서 키워드-시드 간 그래프 구성
- BFS로 연결요소(connected components) 탐색
- 각 연결요소를 하나의 도메인으로 간주
- 중심성(centrality) 기반으로 핵심 키워드 선정 → 도메인명 생성
- 그래프가 없을 경우 카테고리 기반 폴백
처리 파이프라인 (AutoSeedPipeline)
1단계: 후보 생성 (SeedCandidateGenerator)
TextRecord의 text에서 키워드 후보를 추출.
형태소 분석기 (kiwipiepy 설치 시):
- 명사(NNG, NNP), 외국어(SL), 숫자(SN) 토큰 추출
- 연속 명사 구합 (1~4단어)
- bi-gram, tri-gram 생성
정규식 fallback (kiwipiepy 미설치 시):
[0-9a-zA-Z가-힣]{2,}패턴으로 토큰 추출- bi-gram, tri-gram 생성
2단계: 정제 (KeywordCleaner)
후보에서 노이즈 제거.
필터 규칙:
- 텍스트 길이 < 2 제거
- 토큰 수 1~4개만 유지
- 불용어 제거 (방법, 후기, 정리, 진짜, 너무 등)
- 숫자만 있는 표현 제거
- URL 포함 표현 제거
- 모든 토큰이 불용어인 경우 제거
3단계: 군집화 (KeywordClusterer)
유사한 표현을 하나의 클러스터로 묶음.
유사도 판정 (셋 중 하나라도 만족하면 동일 클러스터):
- 포함관계: 짧은 쪽이 긴 쪽의 75% 이상 차지
- Fuzzy matching:
rapidfuzz.ratio()≥ 90 - Cosine similarity: TF-IDF char_wb ngram 코사인 ≥ 0.82
대표 키워드 선정 기준:
- 1~2 토큰이 유리 (보너스 1.0 vs 0.7)
- 너무 긴 표현은 패널티
- 비즈니스 관련어 포함 시 우대
4단계: 점수화 (SeedScorer)
각 대표 키워드에 종합 점수 부여.
점수 공식:
seed_score =
source_diversity_norm × 0.30 ← 여러 소스에서 등장
+ frequency_norm × 0.25 ← 자주 등장
+ growth_norm × 0.20 ← 최근 증가 추세
+ expandability_norm × 0.15 ← 하위 키워드 확장성
+ business_norm × 0.10 ← 비즈니스 관련성
소스별 가중치:
| 소스 | 가중치 | |------|--------| | internal_search | 1.00 | | naver_datalab_topic | 0.90 | | google_trends_topic | 0.90 | | autocomplete | 0.85 | | community_title | 0.75 | | faq | 0.70 | | product_catalog | 0.70 | | review | 0.65 | | blog_title | 0.60 | | news_title | 0.55 |
승격 기준: seed_score ≥ 65 → is_promoted_seed = True
5단계: 분류 (Classifiers)
IntentClassifier (검색 의도)
| 분류 | 힌트 키워드 | |------|------------| | 구매형 | 추천, 가격, 할인, 순위, 브랜드, 가성비, 구매 | | 정보형 | 뜻, 방법, 효능, 원인, 정의, 비교, 차이 | | 문제해결형 | 안됨, 오류, 해결, 고장, 증상 | | 속성형 | 저분자, 무선, 고단백, 민감성, 여성, 30대 등 | | 토픽허브형 | 위 분류에 해당하지 않는 일반 토픽 |
TrendPeriodClassifier (시간 트렌드)
3개 기간(최근 7일 / 최근 30일 / 직전 달)의 출현 패턴 비교.
| 분류 | 조건 | |------|------| | 신규급등 | recent_7d에만 등장 | | 급상승 | recent에 등장, prev_month에 없음 | | 지속상승 | prev_month 대비 순위 10위 이상 상승 | | 안정 | 모든 기간에 꾸준히 등장 | | 보합 | recent_30d에만 등장 | | 하락 | prev_month에만 있고 recent에 없음 |
SQLite 테이블
매 실행 덮어쓰기 (seed_results.db)
| 테이블 | 설명 |
|--------|------|
| clustered | 군집화 결과 (정규화된 키워드 → 클러스터 매핑) |
| scored | 점수화 결과 (대표 키워드별 종합 점수) |
| seed_report | 최종 리포트 (상위 50개) |
영구 누적 (seed_results.db)
| 테이블 | 설명 |
|--------|------|
| keyword_snapshots | 매 실행 수집 키워드 이력 (keyword, source, domain, rank, period, category) |
| discovered_domains | 자동 발견된 도메인 (domain_name PK, core_keywords, keyword_count) |
| expansion_queue | 키워드 확장 대기열 (keyword, status: pending/done) |
환경변수 (.env)
# 네이버 DataLab 공식 API (현재 미사용, 직접 수집 방식 사용 중)
NAVER_CLIENT_ID=...
NAVER_CLIENT_SECRET=...
# Google Trends 공식 API alpha (접근 권한 필요시)
GOOGLE_TRENDS_ALPHA_ENABLED=false
GOOGLE_TRENDS_ALPHA_BASE_URL=
GOOGLE_TRENDS_ALPHA_TOKEN=
# 자동완성 수집기 (선택, GenericJSONAutocompleteCollector용)
AUTOCOMPLETE_ENABLED=false
AUTOCOMPLETE_URL=
AUTOCOMPLETE_METHOD=GET
AUTOCOMPLETE_QUERY_PARAM=q
AUTOCOMPLETE_PLATFORM=
의존성 (pyproject.toml)
| 패키지 | 용도 | 필수 | |--------|------|------| | pandas | 데이터 처리 | O | | numpy | 수치 계산 | O | | requests | HTTP 요청 | O | | rapidfuzz | 퍼지 문자열 매칭 | O | | scikit-learn | TF-IDF 코사인 유사도 | O | | python-dateutil | 날짜 처리 | O | | python-dotenv | 환경변수 로드 | O | | pytrends | Google Trends 수집 | O | | beautifulsoup4 | HTML 파싱 | O | | kiwipiepy | 한국어 형태소 분석 | X (없으면 정규식 fallback) |
클래스 관계도
HTTPClient ──────────────────────────────────────────────────┐
│ │
├── NaverShoppingInsightCollector │
│ → list[TextRecord] (9카테고리 × 3기간 × 2페이지) │
│ │
├── GoogleTrendingRSSCollector │
│ → list[TextRecord] (실시간 급상승 검색어) │
│ │
├── NaverNewsTrendingCollector │
│ → list[TextRecord] (인기 뉴스 제목) │
│ │
├── PyTrendsCollector │
│ → list[TextRecord] (Google 관련 키워드) │
│ │
├── NaverAutocompleteCollector │
│ → list[TextRecord] (네이버 자동완성) │
│ │
├── NaverRelatedSearchCollector │
│ → list[TextRecord] (네이버 연관검색어) │
│ │
├── NaverDataLabDirectCollector │
│ → list[TextRecord] + list[KeywordTrendPoint] │
│ │
└── GenericJSONAutocompleteCollector (선택) │
AutoSeedPipeline
├── KoreanPhraseExtractor (형태소/정규식)
├── SeedCandidateGenerator (후보 생성)
├── KeywordCleaner (정제)
├── KeywordClusterer (군집화: fuzzy + TF-IDF)
├── ExpandabilityEstimator (확장성 평가)
├── BusinessRelevanceScorer(비즈니스 관련성)
├── GrowthCalculator (성장률)
├── SeedScorer (종합 점수)
├── IntentClassifier (검색 의도)
└── TrendPeriodClassifier (시간 트렌드)
DomainDiscoverer
└── discover(records) → list[DomainCluster]
(공동출현 네트워크 → BFS 연결요소 → 도메인 추출)
run_cycle() ← main()에서 호출
├── A. 트렌드 수집 (쇼핑인사이트 + pytrends + Google RSS + 뉴스)
├── B. 확장 큐 처리 (pending → 자동완성 + 연관검색어 → done)
├── C. 상위 키워드 확장 (자동완성 + 연관검색어)
├── D. 도메인 자동 발견 (DomainDiscoverer)
└── E. 점수화 + 영구 저장 (keyword_snapshots + expansion_queue)
Rate Limit 대응
| 수집기 | 대응 방식 | |--------|----------| | NaverShoppingInsightCollector | 요청 간 0.8초 대기 | | NaverDataLabDirectCollector | 요청 간 1.0초 대기 | | PyTrendsCollector | suggestions() 호출 간 0.5초 대기 | | NaverAutocompleteCollector | 요청 간 0.3초 대기 | | NaverRelatedSearchCollector | 요청 간 0.5초 대기 | | NaverNewsTrendingCollector | 단일 요청 (페이지당 1회) | | GoogleTrendingRSSCollector | 단일 요청 (피드당 1회) | | 공통 | HTTPClient 재시도 2회, 지수 백오프 |