# 벡터DB 큐레이션 API 명세 (웹 개발자 핸드오프) 벡터DB(Qdrant)를 웹에서 검색/조회/삭제/추가하기 위한 백엔드 API입니다. 프론트엔드는 이 명세에 맞춰 개발하면 됩니다. (백엔드: `scripts/admin_service.py`) --- ## 1. 접속 정보 | 항목 | 값 | |------|-----| | Base URL | `http://: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=&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://:28013/api/stats # 검색 curl -X POST http://:28013/api/search \ -H "Content-Type: application/json" \ -d '{"query":"단말기 구입","top_k":5}' # 목록 curl "http://:28013/api/points?limit=10" # 추가 curl -X POST http://:28013/api/points \ -H "Content-Type: application/json" \ -d '{"q":"테스트 질문","a":"테스트 답변","category":"기타"}' # 수정 curl -X PUT http://:28013/api/points/ \ -H "Content-Type: application/json" \ -d '{"a":"수정된 답변"}' # 키워드(문자열 포함) 검색 curl -X POST http://:28013/api/keyword-search \ -H "Content-Type: application/json" \ -d '{"keyword":"1588-2504","field":"a","limit":100}' # 필터 검색/목록 curl -X POST http://:28013/api/search \ -H "Content-Type: application/json" \ -d '{"query":"단말기","source":"hipass_faq"}' curl "http://:28013/api/points?limit=10&category=하이패스 이용" # 삭제 curl -X DELETE http://:28013/api/points/ ``` --- ## 7. 웹 개발자에게 함께 전달할 정보 체크리스트 - [ ] Base URL / 포트 (`http://:28013`) - [ ] 이 문서(엔드포인트·스키마·에러·페이지네이션 방식) - [ ] `GET /docs` Swagger 주소 (실시간 스키마 확인) - [ ] 데이터 필드 의미 (`q/a/category/source/source_id/url/score`) - [ ] 삭제/수정 키 = `id`(결정적 UUID) 규칙 - [ ] CORS 허용 정책 / 인증 유무(현재 없음) - [ ] 수정은 `PUT /api/points/{id}` (응답 `updated`/`moved` 처리 안내) - [ ] 필터(`category`/`source`)는 정확일치만 지원 (부분검색 아님) - [ ] (향후) 실패질문 목록·파일 업로드 기능 추가 예정 여부