# 설계서: 기술 태그 기반 벡터 지식자료 검색과 VPD 연결

> **상태**: 제품형 운영 흐름 구현 · DDS 병행 검증 완료 · 객체별 Data Grant predicate 적용 · 운영 확장 항목 별도
> **추적성**: Redmine #565, #566 · 구현 커밋 `a5e70bc`, `04cca64`, `8978be4` · 기준 구현: `28_agent_ords_vector_tag_vpd_setup.sql`, `29_agent_ords_vector_search_ords.sql`, `32_dds_vector_tag_setup.sql`, `34_dds_token_data_grant_common_auth.sql`, `36_dds_sales_knowledge_scenario.sql`

## 1. 한 문장으로 이해하기

문서를 잘게 나누고(청킹) 숫자 벡터로 바꾼 뒤(벡터화) 각 조각에 `SALES`, `HR`, `FINANCE` 같은 업무 접근 분류값을 붙인다. 사용자의 권한에는 “이 값을 볼 수 있음”을 등록하고, 검색은 권한에 맞는 조각만 검색 결과에 남긴다.

이 기능에서 `TECH_TAG`는 문서 청크에 저장된 업무 분류 컬럼 값이고, VPD/DDS는 그 값을 이용해 DB가 실제로 보여 줄 행을 제한하는 장치다. 화면에서 검색 조건을 넣는 것만으로는 우회할 수 없도록 DB 안에서 마지막 필터를 적용한다.

## 2. 큰 흐름 (일반 사용자가 보는 순서)

```text
지식자료 등록
  → 문서를 문단 단위 청크로 나눔
  → 각 청크를 임베딩 모델로 벡터화
  → 청크마다 TECH_TAG를 하나 이상 부여
  → ADMIN.CB_VECTOR_SEARCH_DOCUMENTS 뷰에 노출

권한 설계
  → 역할에 SELECT 권한 부여
  → 행 규칙에서 특정 기술 태그(TAG)를 여러 개 등록
  → 같은 ALLOW 권한의 태그는 OR(하나라도 일치하면 허용)
  → DENY 태그는 허용 후보에서 다시 제외

검색
  → 호출자가 Bearer 토큰을 보냄
  → ORDS가 토큰으로 사용자 컨텍스트를 설정
  → VPD가 TECH_TAG 권한에 맞지 않는 행을 먼저 제거
  → 남은 청크만 벡터 거리순으로 정렬해 반환
```

## 3. 마이크로 동작 (DB에서 실제로 일어나는 일)

### 3.1 저장 구조

| 객체 | 역할 |
|---|---|
| `CB_VECTOR_DOCUMENT_CHUNK` | 문서 ID, 청크 번호, 제목, 본문, 원본 주소, 임베딩 벡터 저장 |
| `CB_VECTOR_DOCUMENT_TAG` | 청크와 기술 태그의 다대다 연결. 한 청크에 태그 여러 개 가능 |
| `CB_VECTOR_SEARCH_DOCUMENTS` | 청크 한 개당 한 행으로 태그를 쉼표로 합친 검색용 뷰. VPD를 이 뷰에 부착 |
| `CB_PERMISSION_RULE` | 기존 권한 체계에 `rule_type = TAG`, `rule_value = 태그`로 저장 |

`TECH_TAG`는 VPD가 읽는 행 접근 기준이다. `EMBEDDING`의 민감도나 `CHUNK_TEXT`의 표시 방식은 별도의 컬럼 표시 보호 정책이며, 태그 권한과 섞지 않는다.

### 3.2 TAG 규칙

- 컬럼을 비워 두면 기본 컬럼 `TECH_TAG`를 사용한다.
- 태그 값은 저장할 때 대문자로 정규화한다. 예: `spring_boot` → `SPRING_BOOT`.
- 같은 `ALLOW` 권한에 태그를 여러 개 넣으면 OR이다.

```text
ALLOW TAG SALES
ALLOW TAG INTERNAL

결과: TECH_TAG 목록에 SALES가 포함됨
   OR TECH_TAG 목록에 INTERNAL이 포함됨
```

- `DENY` 태그도 내부적으로 OR로 묶은 뒤 허용 결과에서 뺀다.

```text
(허용 태그 A OR 허용 태그 B)
AND NOT (거부 태그 C OR 거부 태그 D)
```

- 허용 규칙이 없거나 태그 컬럼이 실제 객체에 없으면 `1 = 0`으로 닫는다(fail-closed).
- 사용자가 여러 역할을 가지고 있어도 허용 태그는 합쳐져 OR가 된다. 어떤 역할의 DENY 태그와 일치하면 최종 결과에서 제외된다.

## 4. 예시 시나리오

기본 제공 지식자료에는 다음과 같은 검색 단위가 있다.

| 청크 | 태그 |
|---|---|
| 세일즈 파이프라인 운영 가이드 | `SALES`, `INTERNAL` |
| 인사 채용 운영 기준 | `HR`, `INTERNAL` |
| 재무 마감 체크리스트 | `FINANCE`, `INTERNAL` |

역할을 다음처럼 만든다.

1. `SALES_KNOWLEDGE_ROLE`: `ALLOW TAG SALES`
2. `HR_KNOWLEDGE_ROLE`: `ALLOW TAG HR`
3. `KNOWLEDGE_INTERNAL`: `ALLOW TAG INTERNAL`, 필요 시 `DENY TAG FINANCE`

그러면:

- `agent_sales` 사용자는 `SALES` 값이 붙은 세일즈 청크만 본다.
- `agent_hr` 사용자는 `HR` 값이 붙은 청크만 본다.
- `KNOWLEDGE_INTERNAL` 사용자는 `INTERNAL` 값이 붙은 청크를 보되, `FINANCE` 거부 규칙이 있으면 재무 청크는 제외된다.
- 태그 권한이 전혀 없는 사용자는 결과가 없다.

## 5. ORDS API 계약

### 요청

```http
POST /ords/cb-ords/cb-agent-vector/search?limit=10
Authorization: Bearer <발급된 토큰>
Content-Type: application/json

{"embedding":[0.10,0.20,0.30,0.40]}
```

`embedding`은 검색어를 외부 임베딩 모델로 변환한 벡터다. DB는 임베딩 모델을 호출하지 않는다. 따라서 모델 선택·차원·재임베딩 일정은 지식자료 파이프라인에서 관리한다.

### 응답

```json
{
  "items": [
    {
      "chunk_id": 28001,
      "document_id": "knowledge-001",
      "chunk_no": 1,
      "title": "세일즈 파이프라인 운영 가이드",
      "chunk_text": "...",
      "source_uri": "kb://business/sales/pipeline",
      "tech_tag": "INTERNAL,SALES",
      "score": 0.0123
    }
  ]
}
```

`CB_VECTOR_SEARCH_DOCUMENTS`는 청크 한 개당 한 행을 만들고 `TECH_TAG`에 태그 목록을 담는다. 그래서 VPD가 허용/거부를 청크 전체에 적용할 수 있다. ORDS 핸들러는 VPD를 통과한 청크를 벡터 거리순으로 정렬한다. `EMBEDDING` 자체는 응답에 포함하지 않는다.

## 6. 설치·등록 순서

운영 DB에 자동 실행하지 않고 다음 순서로 승인된 환경에서 실행한다.

1. `25_agent_ords_security_backoffice_support.sql` — 기존 backoffice 권한 메타데이터
2. `31_agent_ords_backoffice_ux_metadata.sql` — 기존 설치에 조회 대상 설명/정책 설명 메타데이터 추가
3. `26_agent_ords_security_dynamic_vpd_filter.sql` — `TAG` 분기를 포함한 동적 VPD 함수
4. `28_agent_ords_vector_tag_vpd_setup.sql` — VECTOR 테이블·태그 테이블·뷰·VPD·보호 객체 등록
5. `29_agent_ords_vector_search_ords.sql` — `cb-agent-vector/search` ORDS 모듈/핸들러 (CB_ORDS로 실행)
6. backoffice `/permissions`에서 역할별 `특정 기술 태그` 규칙을 저장
7. `30_agent_ords_vector_tag_vpd_test.sql` — OR와 DENY 우선순위 확인
8. DDS 기본 경로: `34_dds_token_data_grant_common_auth.sql` — 단일 기술 사용자·객체별 Data Grant predicate
9. 세일즈 시나리오: `36_dds_sales_knowledge_scenario.sql` — `agent_sales` + `TECH_TAG=SALES` + 단기 데모 토큰

backoffice의 `/objects` 화면은 이 객체에 일반 Handler를 생성하지 않도록 막는다. 일반 Handler를 사용하면 벡터 컬럼을 그대로 반환할 수 있기 때문에, 임베딩을 응답에서 제외하고 벡터 거리 검색을 하는 전용 Handler만 허용한다.

실제 지식자료를 넣을 때는 샘플 청크 대신 ingestion 작업이 다음을 반복한다.

```text
원문 → 청크 분할 → 임베딩 생성 → CB_VECTOR_DOCUMENT_CHUNK 저장
                                  → CB_VECTOR_DOCUMENT_TAG에 태그 저장
```

백오피스의 `/vector-knowledge` 화면에서 이 흐름을 운영 절차와 같은 순서로 확인할 수 있다.

- 문서 ID·제목·본문·기술 태그를 입력하면 문단/길이 기준으로 청크를 만든다.
- `DEMO-4D`는 외부 API 없이 기본 제공 자료와 같은 4차원 벡터를 만드는 개발용 로컬 임베딩 모드다.
- `AI`를 선택하면 `BACKOFFICE_AI_BASE_URL/v1/embeddings`와
  `BACKOFFICE_AI_EMBEDDING_MODEL`을 사용해 청크마다 실제 임베딩을 생성한다.
- 같은 방식으로 검색어를 임베딩하고, 임시 토큰으로 전용 ORDS Handler를 호출한다.
- VPD가 `TECH_TAG`를 먼저 필터링한 뒤 남은 청크만 벡터 거리순으로 반환한다.

`DEMO-4D`는 의미 기반 품질을 보장하는 모델이 아니라 권한 흐름을 확인하기 위한 고정 차원 개발 모드다. 운영 검색은 ingestion과 검색에 같은 임베딩 모델·차원을 사용해야 한다.

## 7. 운영 가이드라인

- 값은 자유 문장보다 대문자·언더스코어 형태의 안정적인 ID로 관리한다. 예: `SALES`, `HR`, `FINANCE`, `INTERNAL`.
- 현재 예시의 태그 ID 허용 문자는 영문·숫자·`_`·`-`다. 이 범위를 벗어난 값은 UI와 VPD 함수에서 모두 무시해 권한이 넓어지지 않게 한다.
- 동의어를 무분별하게 만들지 말고 분류값 사전을 먼저 정한다. `SALES`, `SALES_TEAM`, `SELLING`을 모두 별개로 만들면 권한 누락이 생긴다.
- 문서 청크에 최소 한 개의 업무 분류값을 부여한다. 값이 없는 청크는 이 뷰에 노출되지 않는다.
- 분류값을 수정하면 권한 결과가 즉시 바뀐다. 임베딩을 다시 만들 필요가 없는 값 변경과, 내용이 바뀌어 재임베딩해야 하는 변경을 구분한다.
- `TECH_TAG`는 행 접근용이고, 컬럼 민감도/마스킹은 별도 정책이다. “컬럼을 PUBLIC으로 표시했다”는 사실이 행 조회 권한을 부여하지 않는다.
- 벡터 차원은 ingestion 모델과 테이블 데이터가 일치해야 한다. 차원이 다르면 ORDS가 4xx/5xx 오류를 반환하므로 모델 버전을 함께 기록한다.

## 8. 검증 기준

- 같은 ALLOW 권한의 태그 두 개가 각각 일치하는 청크를 반환한다.
- ALLOW 태그와 DENY 태그가 함께 있으면 DENY 일치 청크는 반환되지 않는다.
- 태그 권한이 없는 사용자, 잘못된 토큰, 존재하지 않는 태그는 결과가 0건 또는 차단으로 끝난다.
- ORDS 응답에는 허용되지 않은 태그의 청크가 섞이지 않는다.
- 검색 결과에 임베딩 원문이 포함되지 않는다.
- `/permissions` 화면에서 `TAG` 규칙이 기본 `TECH_TAG`와 OR 의미를 일반 문장으로 설명한다.

## 9. 아직 별도 운영 설계가 필요한 부분

- 대용량 파일 업로드, 비동기 작업 큐, 재시도·실패 격리, 임베딩 모델 버전별 재색인은 운영 파이프라인에서 별도로 설계한다.
- 태그 사전 승인 워크플로와 문서별 소유자/보존기간 정책은 현재 화면의 범위를 넘어선다.
- 운영 DB에 대한 DDL, 기존 정책 교체, 방화벽/NSG 변경은 별도 승인을 받아 실행한다.

## 10. DDS 병행 시나리오: 같은 TAG 권한, 단일 기술 사용자

VPD에서 검증한 기술 태그 권한을 DDS에서도 고객별 DDS 계정으로 복제하지 않고 적용할 수 있는지 별도로 확인했다. 업무 권한의 원천은 동일한 `CB_PERMISSION`·`CB_PERMISSION_RULE`이다. DDS 설치 스크립트는 이 규칙을 벡터 검색 뷰의 객체별 Data Grant predicate와 연결한다. 토큰 함수는 먼저 `CB_AGENT_CTX`에 업무 사용자를 설정하고, Data Grant predicate가 그 Context와 공통 권한 테이블을 사용해 각 행을 평가한다.

```text
Bearer token
  → CB_AGENT_BEARER_KEY에서 사용자 식별
  → CB_APP_USER의 직접 역할/활성 그룹 역할 계산
  → ALLOW TAG는 OR, DENY TAG는 NOT EXISTS
  → CB_DDS_VECTOR_SEARCH_DOCUMENTS의 DATA GRANT predicate 평가
  → 허용된 청크 반환
```

하나의 `dds_demo_token` DDS 기술 사용자 세션에서 `CB_AGENT_CTX_PKG.SET_USER_BY_BEARER(token)`을 호출한 뒤 객체별 `ADMIN.DDS_DEMO_TOKEN_VECTOR_GRANT`가 공통 권한을 평가한 결과는 다음과 같다.

| 토큰 사용자 | 반환 청크 |
|---|---|
| `agent_hr` | `28001`, `28002` (2건) |
| `agent_fin_self` | `28002` (1건) |
| `agent_all` | `28001`, `28002`, `28003` (3건) |
| `agent_sales` | `28004` (1건, `TECH_TAG=SALES`) |

이 방식의 의미는 “DDS가 토큰 문자열을 Data Grant 문법의 인자로 받는다”가 아니다. `DATA GRANT`는 `ON` 대상 뷰와 저장된 SQL predicate를 선언하고, 토큰 함수가 그 predicate가 참조할 Context를 만든다. 따라서 현재 제품형 기본 시나리오는 **단일 DDS 기술 사용자 + 객체별 DATA GRANT predicate + 공통 업무 권한**이다. 여러 보호 뷰/테이블에는 각각 Data Grant를 만들고, 각 predicate에서 논리 권한 대상과 행 규칙을 매핑한다. 드라이버가 SQL 실행 전에 `EndUserSecurityContext`를 전달하는 순수 DDS Context 방식은 별도 확장 경계이며, 그 경우에는 `ORA_END_USER_CONTEXT`를 참조하는 일반 `DATA GRANT`를 설계한다.

토큰, 임시 함수, 임시 권한은 검증 뒤 삭제했다. 새 태그나 역할을 추가할 때도 VPD와 DDS가 각각 고객 사용자를 복제하는 대신, 공통 `CB_*` 권한을 먼저 수정하고 두 집행 경계의 회귀 테스트를 함께 실행한다.
