발행된 사이트에 업체별 SEO/AEO 키워드를 제공하는 서비스. - PostgreSQL 16 + pgvector/ltree/pg_trgm 단일 스토어 (정확·의미·계층 조회를 한 엔진에서 처리) - 키워드는 전역 사전 + merchant_keyword 연결 테이블 구조 - 4단계 계단식 중복제거: 금칙어 → normalized 완전일치 → pg_trgm → 코사인 유사도, 걸린 표기는 aliases[] 로 흡수 - BullMQ 생성 큐 (발행 즉시 / 일 1회 크론 / 성과 기반) - OpenAI Structured Outputs + mock provider (API 키 없이 로컬 전 구간 동작) - 서빙 API: /v1/sites/:id/seo, /aeo, /performance, /keywords/search - docs/architecture.html 설계 도식 JSON-LD 조립과 o2o-site-AEO 연동은 후속 작업. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| docs | ||
| drizzle | ||
| scripts | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| nest-cli.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
o2o-site-ontology
o2o-site-AEO 가 발행한 사이트에 업체별 SEO/AEO 키워드를 제공하는 온톨로지 서비스.
- 주기적으로 LLM 에게 업체 정보를 주고 키워드·태그·Q&A 를 생성
- 4단계 계단식 중복제거로 전역 키워드 사전을 오염 없이 유지
- 발행 사이트는 REST 로 SEO/AEO payload 만 받아 쓴다 (JSON-LD 조립은 후속 단계)
빠른 시작 (로컬)
npm install
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 start # http://localhost:3100
npm run smoke # (다른 터미널) 엔드투엔드 점검
npm run db:reset 은 볼륨까지 지우고 migrate + seed 를 다시 돌린다.
실제 OpenAI 로 전환
# .env
LLM_PROVIDER=openai
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4.1-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
LLM_PROVIDER=mock 은 문자 bigram 해싱 임베딩을 쓴다. 랜덤이 아니라 비슷한 문자열이면
비슷한 벡터가 나오므로 중복제거 파이프라인 검증에는 충분하지만, 의미 기반 중복
(강남 미용실 ↔ 강남 헤어샵) 은 실제 임베딩 모델에서만 잡힌다.
데이터 모델
| 테이블 | 역할 |
|---|---|
industry / region |
ltree 업종·지역 계층. 상위 노드 키워드 상속의 기반 |
merchant |
업체. external_id 가 o2o-site-AEO 의 사이트 ID |
keyword |
전역 키워드 사전. normalized 유니크, aliases[], embedding vector(1536) |
merchant_keyword |
업체 ↔ 키워드 연결. relevance / status / impressions / ctr |
qa_pair |
AEO 용 질문-답변 쌍 |
generation_run |
생성 감사 로그 (프롬프트 버전·토큰·통계) |
키워드는 업체에 복제하지 않고 전역 사전 + 연결 테이블로 둔다. 그래야 임베딩이 하나만
저장되고, 강남 미용실 을 쓰는 업체가 100곳이어도 중복제거가 성립한다.
중복제거 4단계
값비싼 벡터 비교를 마지막에 두고, 후보 집합 안에서만 수행한다.
| 단계 | 방법 | 걸러내는 것 |
|---|---|---|
| 0 | 금칙어 필터 | 최고, 1위, 100% 등 과장광고 |
| 1 | normalized 완전 일치 (공백·구두점 제거) |
강남 뿌리 염색 = 강남 뿌리염색 |
| 2 | pg_trgm 유사도 ≥ 0.6 |
강남 뿌리염색약 → 강남 뿌리염색 |
| 3 | 코사인 유사도 ≥ 0.92 | 강남 미용실 ↔ 강남 헤어샵 (의미 중복) |
| 4 | 신규 등록 | 위에 안 걸리면 새 키워드 |
1~3 단계에서 매칭되면 원래 표기는 버리지 않고 기존 키워드의 aliases[] 로 흡수한다
(롱테일 검색어 보존 + 성과 피드백 매칭에 사용).
임계값은 .env 의 DEDUP_COSINE_THRESHOLD / DEDUP_TRIGRAM_THRESHOLD 로 조정.
API
| 메서드 | 경로 | 용도 |
|---|---|---|
GET |
/health |
헬스체크 |
POST |
/v1/merchants/publish |
사이트 발행 웹훅 — 업체 upsert + 생성 예약 (sync:true 면 동기 실행) |
POST |
/v1/merchants/:id/generate?sync=true |
수동 재생성 |
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/keywords/search |
의미 기반 키워드 검색 (어드민) |
POST |
/v1/sites/:id/performance |
Search Console·유입 로그 피드백 → 저성과 키워드 강등 |
:id 는 external_id 또는 내부 UUID 둘 다 받는다.
발행 웹훅 예시
curl -X POST http://localhost:3100/v1/merchants/publish \
-H 'content-type: application/json' \
-d '{
"externalId": "site-1003",
"name": "강남 뷰티랩",
"industryId": "beauty.hair",
"regionId": "kr.seoul.gangnam",
"description": "강남 미용실. 염색 전문.",
"profile": { "services": ["뿌리염색", "여성펌"], "features": ["주차가능"] }
}'
서빙 예시
curl 'http://localhost:3100/v1/sites/site-1001/seo?limit=8'
{
"title": "레브살롱 | 강남 미용실",
"description": "강남역 3번 출구 앞 프라이빗 헤어살롱. ... 정보를 확인하세요.",
"keywords": ["레브살롱", "강남 미용실", "강남 남자 커트", "..."],
"tags": [{ "keyword": "강남 미용실", "intent": "local", "relevance": 0.95, "aliases": ["강남미용실"] }]
}
생성 주기
- 발행 즉시 —
/v1/merchants/publish가 BullMQ 에 적재 (60초 dedupe 창) - 주기 리프레시 — 매일 03:00 크론이
REFRESH_INTERVAL_DAYS(기본 30일) 지난 업체를 적재 - 성과 기반 — 노출 100회 이상 & CTR < 0.2% 인 키워드는
demoted로 강등, 다음 사이클에서 대체
프롬프트에는 해당 업체와 같은 업종의 기존 키워드 목록을 넣어 중복 후보 생성 자체를 줄인다. 그래도 남는 중복만 위 4단계가 처리한다.
남은 작업
- JSON-LD (
LocalBusiness/FAQPage/Service) 조립 —structuredDataHints를 그대로 매핑 /llms.txt서빙- 업종
ltree상위 노드 키워드 상속 (source: 'inherited') - Redis 응답 캐시 (서빙은 읽기 99%)
- Search Console API 연동 (현재는
/performance수동 주입) - 어드민 UI
아키텍처 도식
docs/architecture.html 을 브라우저로 열면 전체 흐름·중복제거 단계·데이터 모델을 볼 수 있다.