Files
exAichatbot_agent/exAiChatBot-chatbot2.0-agent/VECTORDB_ADMIN_API.md
T
Macbook 4b86b2a660 Agent 2.0 exdev 서버 배포 스택
- server-dev start/stop/deploy 및 Gitea push 자동 배포
- local-dev 로컬 개발 환경

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:57:30 +09:00

309 lines
10 KiB
Markdown

# 벡터DB 큐레이션 API 명세 (웹 개발자 핸드오프)
벡터DB(Qdrant)를 웹에서 검색/조회/삭제/추가하기 위한 백엔드 API입니다.
프론트엔드는 이 명세에 맞춰 개발하면 됩니다. (백엔드: `scripts/admin_service.py`)
---
## 1. 접속 정보
| 항목 | 값 |
|------|-----|
| Base URL | `http://<WAS서버>:28013` (포트는 `.env`의 `ADMIN_PORT`, 기본 28013) |
| 형식 | JSON (요청/응답 모두 `application/json`) |
| 인증 | **현재 없음** (내부망 전용). 외부 노출 시 별도 인증 필요 |
| CORS | 기본 전체 허용(`*`). 제한하려면 `ADMIN_CORS_ORIGINS="http://host:port,..."` |
| 문서 | `GET /docs` (FastAPI 자동 Swagger UI) |
> 같은 서버에 **레퍼런스용 단일 페이지 UI**(`GET /`)가 이미 떠 있습니다. 동작 확인/참고용이며, 실제 프론트는 아래 API로 자유롭게 구현하면 됩니다.
---
## 2. 데이터 모델 (point = FAQ 1건)
검색/조회 응답의 항목(item) 공통 형태:
```jsonc
{
"id": "3d6f...e9", // Qdrant point ID (결정적 UUID). 삭제/조회의 키
"q": "하이패스 단말기는 어디서 구입하나요?", // 질문
"a": "하이패스 단말기는 ...", // 답변
"category": "하이패스 이용", // 분류 (nullable)
"source": "hipass_faq", // 출처 (nullable)
"source_id": "ser_no_1", // 원본 식별자 (nullable)
"url": "https://...", // 공식 FAQ 링크 (nullable)
"score": 0.87 // 검색 결과에서만 포함 (유사도, 0~1)
}
```
### ID 규칙 (중요)
- `id`는 `source:source_id`(없으면 질문 텍스트) 기반 **결정적 UUID**입니다.
- 같은 FAQ는 항상 같은 `id` → **삭제/수정 시 이 `id`를 키로 사용**하면 됩니다.
- 같은 항목을 다시 추가하면 **덮어쓰기(upsert)** 되어 중복이 생기지 않습니다.
---
## 3. 엔드포인트
### 3.1 통계
```
GET /api/stats
```
응답:
```json
{ "vector_store": "qdrant", "count": 3421 }
```
### 3.2 의미 검색
```
POST /api/search
Content-Type: application/json
```
요청:
```json
{ "query": "단말기 어디서 사요?", "top_k": 20, "threshold": null, "category": null, "source": null }
```
| 필드 | 필수 | 설명 |
|------|------|------|
| `query` | ✅ | 검색어 (1~500자) |
| `top_k` | ✕ | 결과 수 (기본 20, 1~100) |
| `threshold` | ✕ | 최소 유사도 (미지정 시 전체) |
| `category` | ✕ | 분류 **정확일치** 필터 |
| `source` | ✕ | 출처 **정확일치** 필터 |
응답:
```json
{
"count": 2,
"items": [
{ "id": "3d6f...", "q": "...", "a": "...", "category": "...", "source": "...", "source_id": "...", "url": "...", "score": 0.87 }
]
}
```
### 3.2-b 키워드(문자열 포함) 검색
```
POST /api/keyword-search
Content-Type: application/json
```
의미 검색과 달리 **q/a 텍스트에 입력 문자열이 실제 포함된 항목만** 반환합니다(부분일치, 대소문자 무시). DB 전체를 스캔합니다.
요청:
```json
{ "keyword": "1588-2504", "field": "both", "category": null, "source": null, "limit": 50 }
```
| 필드 | 필수 | 설명 |
|------|------|------|
| `keyword` | ✅ | 포함 검색할 문자열 (1~200자) |
| `field` | ✕ | `both`(기본) / `q` / `a` — 검색 대상 |
| `category`,`source` | ✕ | 정확일치 필터 |
| `limit` | ✕ | 최대 결과 수 (기본 50, 1~500) |
응답:
```json
{
"count": 12,
"scanned": 3421,
"exhausted": true,
"items": [ { "id": "...", "q": "...", "a": "...", "category": "...", "source": "...", "source_id": "...", "url": "..." } ]
}
```
> - `scanned`: 스캔한 항목 수, `exhausted`: 전체를 끝까지 스캔했는지(`false`면 `limit`에서 끊겨 더 있을 수 있음 → `limit`을 올리거나 필터로 좁히기).
> - score는 없습니다(유사도가 아니라 포함 여부).
### 3.2-c 카테고리 목록
```
GET /api/categories
```
등록된 `category` 값과 건수를 반환합니다(분류 필터/트리용, 전체 스캔).
```json
{ "count": 3, "items": [ { "category": "하이패스 이용", "count": 120 }, { "category": "미납통행료", "count": 80 } ] }
```
### 3.3 목록 (페이지네이션 + 필터)
```
GET /api/points?limit=20&offset=<next_offset>&category=하이패스 이용&source=hipass_faq
```
| 파라미터 | 설명 |
|----------|------|
| `limit` | 페이지 크기 (기본 50) |
| `offset` | 다음 페이지 커서. 첫 페이지는 생략, 이후 응답의 `next_offset` 사용 |
| `category` | 분류 **정확일치** 필터 (선택) |
| `source` | 출처 **정확일치** 필터 (선택) |
응답:
```json
{
"count": 20,
"items": [ { "id": "...", "q": "...", "a": "...", "category": "...", "source": "...", "source_id": "...", "url": "..." } ],
"next_offset": "오리피셜커서값 또는 null"
}
```
> `next_offset`이 `null`이면 마지막 페이지입니다. (커서 기반 페이지네이션 — 페이지 번호가 아니라 커서를 넘김)
### 3.4 단건 조회
```
GET /api/points/{id}
```
응답: item 1건. 없으면 `404`.
### 3.5 단건 수정
```
PUT /api/points/{id}
Content-Type: application/json
```
요청 (전달한 필드만 갱신):
```json
{ "q": "수정된 질문", "a": "수정된 답변", "category": "분류", "url": "링크" }
```
| 필드 | 필수 | 설명 |
|------|------|------|
| `q` | ✕ | 질문 (바뀌면 자동 재임베딩) |
| `a` | ✕ | 답변 |
| `category` | ✕ | 분류 |
| `url` | ✕ | 링크 |
응답:
```json
{ "updated": "point id", "moved": false }
```
> - `source`/`source_id`는 유지되어 보통 **같은 id로 덮어쓰기**됩니다.
> - 단, `source_id`가 없어 질문(q) 기반으로 id가 만들어진 항목은 q 수정 시 id가 바뀝니다. 이 경우 새 id로 추가 후 옛 id를 삭제하며 `"moved": true`로 응답합니다. **프론트는 응답의 `updated`(새 id)로 화면 상태를 갱신**하세요.
### 3.6 단건 삭제
```
DELETE /api/points/{id}
```
응답:
```json
{ "deleted": "3d6f..." }
```
없으면 `404`.
### 3.7 일괄 삭제
```
POST /api/points/delete-batch
Content-Type: application/json
```
요청:
```json
{ "ids": ["3d6f...", "a17c..."] }
```
응답:
```json
{ "deleted": 2 }
```
### 3.8 추가 (텍스트 → 임베딩 → upsert)
```
POST /api/points
Content-Type: application/json
```
요청:
```json
{ "q": "질문", "a": "답변", "category": "분류(선택)", "url": "링크(선택)" }
```
| 필드 | 필수 | 설명 |
|------|------|------|
| `q` | ✅ | 질문 |
| `a` | ✅ | 답변 |
| `category` | ✕ | 분류 |
| `url` | ✕ | 링크 |
| `source` | ✕ | 기본 `admin_manual` |
| `source_id` | ✕ | 미지정 시 자동 생성 |
응답:
```json
{ "created": "생성된 point id", "source_id": "manual_ab12cd34" }
```
> 추가 시 질문이 임베딩 서버(TEI)로 벡터화되어 검색 대상이 됩니다. 임베딩 실패 시 `502`.
---
## 4. 에러 형식
FastAPI 표준 형식입니다.
```json
{ "detail": "에러 메시지" }
```
| 코드 | 의미 |
|------|------|
| 400 | 잘못된 요청 / Qdrant 아닌 스토어에서 호출 |
| 404 | 해당 id 없음 |
| 422 | 요청 본문 검증 실패(필수 누락 등) |
| 502 | 임베딩 서버 호출 실패 |
---
## 4-b. 두 가지 검색의 차이 (프론트에서 분리 제공 권장)
| 검색 | 엔드포인트 | 동작 | 용도 |
|------|-----------|------|------|
| **의미 검색** | `POST /api/search` | 임베딩 유사도(뜻이 비슷) — **챗봇이 실제 찾는 것과 동일** | "이 질문에 챗봇이 뭘 답할까" 확인, 유사/중복 후보 탐색 |
| **키워드 검색** | `POST /api/keyword-search` | 문자열 **실제 포함**(부분일치) | "이 단어/번호가 든 FAQ 전부 찾기"(일괄 수정 등) |
> 의미 검색은 글자가 달라도 뜻이 가까우면 나오고, 키워드 검색은 그 문자열이 들어간 것만 정확히 나옵니다.
## 5. 동작/주의 사항 (프론트 개발 시)
1. **추가/삭제는 즉시 반영**됩니다. 같은 Qdrant를 실시간 `/ask`가 공유하므로, 추가/삭제 후 챗봇 검색에 바로 적용됩니다(재시작 불필요).
2. **삭제/수정 키는 `id`**(결정적 UUID)입니다. 목록/검색 응답의 `id`를 그대로 쓰세요.
3. **수정(update)**: `PUT /api/points/{id}` 사용. 전달한 필드만 갱신되고 질문은 자동 재임베딩됩니다. 응답의 `updated`(새 id)와 `moved` 플래그를 확인해 화면을 갱신하세요(대개 `moved:false`로 id 유지).
4. **페이지네이션은 커서 방식**입니다. `next_offset`을 다음 요청 `offset`으로 넘기세요(페이지 번호 점프는 미지원).
5. **검색 score**는 코사인 유사도(0~1)이며 검색 응답에만 있습니다. 목록 응답에는 없습니다.
6. **인증 없음** — 프론트에서 접근제어가 필요하면 별도 협의.
---
## 6. 빠른 테스트 (curl)
```bash
# 통계
curl http://<WAS서버>:28013/api/stats
# 검색
curl -X POST http://<WAS서버>:28013/api/search \
-H "Content-Type: application/json" \
-d '{"query":"단말기 구입","top_k":5}'
# 목록
curl "http://<WAS서버>:28013/api/points?limit=10"
# 추가
curl -X POST http://<WAS서버>:28013/api/points \
-H "Content-Type: application/json" \
-d '{"q":"테스트 질문","a":"테스트 답변","category":"기타"}'
# 수정
curl -X PUT http://<WAS서버>:28013/api/points/<id> \
-H "Content-Type: application/json" \
-d '{"a":"수정된 답변"}'
# 키워드(문자열 포함) 검색
curl -X POST http://<WAS서버>:28013/api/keyword-search \
-H "Content-Type: application/json" \
-d '{"keyword":"1588-2504","field":"a","limit":100}'
# 필터 검색/목록
curl -X POST http://<WAS서버>:28013/api/search \
-H "Content-Type: application/json" \
-d '{"query":"단말기","source":"hipass_faq"}'
curl "http://<WAS서버>:28013/api/points?limit=10&category=하이패스 이용"
# 삭제
curl -X DELETE http://<WAS서버>:28013/api/points/<id>
```
---
## 7. 웹 개발자에게 함께 전달할 정보 체크리스트
- [ ] Base URL / 포트 (`http://<WAS서버>:28013`)
- [ ] 이 문서(엔드포인트·스키마·에러·페이지네이션 방식)
- [ ] `GET /docs` Swagger 주소 (실시간 스키마 확인)
- [ ] 데이터 필드 의미 (`q/a/category/source/source_id/url/score`)
- [ ] 삭제/수정 키 = `id`(결정적 UUID) 규칙
- [ ] CORS 허용 정책 / 인증 유무(현재 없음)
- [ ] 수정은 `PUT /api/points/{id}` (응답 `updated`/`moved` 처리 안내)
- [ ] 필터(`category`/`source`)는 정확일치만 지원 (부분검색 아님)
- [ ] (향후) 실패질문 목록·파일 업로드 기능 추가 예정 여부