o2o-site-ontology/README.md
hbyang ef7d51da2c 설계 문서 PPTX 덱 추가
docs/architecture.html 내용을 발표용 11장 덱으로 재구성.
전체 흐름·중복제거 4단계·데이터 모델 도식을 이미지가 아니라
네이티브 도형으로 그려 PowerPoint 에서 그대로 편집 가능하다.

- scripts/build-deck.py (python-pptx) 로 재생성 가능
- 슬라이드 경계 이탈 0건, 텍스트 가로 넘침 0건 검증

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 13:50:31 +09:00

6.2 KiB

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[] 로 흡수한다 (롱테일 검색어 보존 + 성과 피드백 매칭에 사용).

임계값은 .envDEDUP_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·유입 로그 피드백 → 저성과 키워드 강등

:idexternal_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 브라우저용 설계 문서 — 전체 흐름 · 중복제거 단계 · 데이터 모델
docs/architecture.pptx 발표용 11장 덱. 도식은 이미지가 아니라 네이티브 도형이라 PowerPoint 에서 바로 편집된다

덱은 python3 scripts/build-deck.py 로 다시 생성한다 (pip install python-pptx 필요). 한글 폰트는 Apple SD Gothic Neo, 코드는 Menlo 로 지정되어 있다 — Windows 에서 열 때는 scripts/build-deck.py 상단의 SANS / MONO맑은 고딕 / Consolas 로 바꿔 다시 생성하면 된다.