|
|
|
|
@@ -2,6 +2,7 @@ package com.cloudhandson.ddsbackoffice.service;
|
|
|
|
|
|
|
|
|
|
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
|
|
|
|
import com.cloudhandson.ddsbackoffice.domain.DdsVectorSearchResult;
|
|
|
|
|
import com.cloudhandson.ddsbackoffice.domain.DdsSqlEvidence;
|
|
|
|
|
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestCommand;
|
|
|
|
|
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestResult;
|
|
|
|
|
import com.cloudhandson.vpdbackoffice.domain.vector.VectorKnowledgeSummary;
|
|
|
|
|
@@ -21,6 +22,7 @@ import java.util.Locale;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
import java.util.Properties;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
@@ -44,17 +46,20 @@ public class DdsVectorKnowledgeService {
|
|
|
|
|
private final OpenAiCompatibleClient embeddingClient;
|
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
private final DdsProperties properties;
|
|
|
|
|
private final DdsSqlEvidenceHistory evidenceHistory;
|
|
|
|
|
|
|
|
|
|
public DdsVectorKnowledgeService(
|
|
|
|
|
VectorKnowledgeService commonVectorService,
|
|
|
|
|
OpenAiCompatibleClient embeddingClient,
|
|
|
|
|
ObjectMapper objectMapper,
|
|
|
|
|
DdsProperties properties
|
|
|
|
|
DdsProperties properties,
|
|
|
|
|
DdsSqlEvidenceHistory evidenceHistory
|
|
|
|
|
) {
|
|
|
|
|
this.commonVectorService = commonVectorService;
|
|
|
|
|
this.embeddingClient = embeddingClient;
|
|
|
|
|
this.objectMapper = objectMapper;
|
|
|
|
|
this.properties = properties;
|
|
|
|
|
this.evidenceHistory = evidenceHistory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public VectorKnowledgeSummary summary() {
|
|
|
|
|
@@ -101,7 +106,11 @@ public class DdsVectorKnowledgeService {
|
|
|
|
|
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
|
|
|
|
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
|
|
|
|
|
|
|
|
|
String sql = "SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
|
|
|
|
String requestId = UUID.randomUUID().toString();
|
|
|
|
|
String marker = "DDS_EVIDENCE:" + requestId;
|
|
|
|
|
String submittedStatement = "지식자료 벡터 검색 · " + properties.vectorObject()
|
|
|
|
|
+ " · 유사도순 정렬 · 결과 " + limit + "건";
|
|
|
|
|
String sql = "SELECT /* " + marker + " */ chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
|
|
|
|
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
|
|
|
|
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
|
|
|
|
+ "FROM " + properties.vectorObject() + " d "
|
|
|
|
|
@@ -135,10 +144,12 @@ public class DdsVectorKnowledgeService {
|
|
|
|
|
String message = rows.isEmpty()
|
|
|
|
|
? "DDS DATA GRANT를 통과한 검색 단위가 없습니다."
|
|
|
|
|
: rows.size() + "개 검색 단위가 DDS DATA GRANT를 통과했습니다.";
|
|
|
|
|
DdsSqlEvidence evidence = collectEvidence(connection, marker, submittedStatement, timeoutSeconds);
|
|
|
|
|
evidenceHistory.record(evidence);
|
|
|
|
|
return new DdsVectorSearchResult(
|
|
|
|
|
normalizedUserKey, userLabel, properties.vectorObject(), normalizedQuery, mode,
|
|
|
|
|
embeddingModel(mode), true, "DDS 권한으로 검색했습니다.", message,
|
|
|
|
|
sessionUser, endUser, null, rows);
|
|
|
|
|
sessionUser, endUser, null, rows, evidence);
|
|
|
|
|
}
|
|
|
|
|
} catch (SQLException exception) {
|
|
|
|
|
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
|
|
|
|
@@ -184,7 +195,11 @@ public class DdsVectorKnowledgeService {
|
|
|
|
|
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
|
|
|
|
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
|
|
|
|
|
|
|
|
|
String sql = "SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
|
|
|
|
String requestId = UUID.randomUUID().toString();
|
|
|
|
|
String marker = "DDS_EVIDENCE:" + requestId;
|
|
|
|
|
String submittedStatement = "지식자료 벡터 검색 · " + properties.vectorObject()
|
|
|
|
|
+ " · 유사도순 정렬 · 결과 " + limit + "건";
|
|
|
|
|
String sql = "SELECT /* " + marker + " */ chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
|
|
|
|
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
|
|
|
|
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
|
|
|
|
+ "FROM " + properties.vectorObject() + " d "
|
|
|
|
|
@@ -229,10 +244,12 @@ public class DdsVectorKnowledgeService {
|
|
|
|
|
: rows.size() + "개 검색 단위가 토큰으로 식별된 업무 사용자("
|
|
|
|
|
+ (resolvedAppUser == null ? "확인 불가" : resolvedAppUser)
|
|
|
|
|
+ ")의 DDS DATA GRANT를 통과했습니다.";
|
|
|
|
|
DdsSqlEvidence evidence = collectEvidence(connection, marker, submittedStatement, timeoutSeconds);
|
|
|
|
|
evidenceHistory.record(evidence);
|
|
|
|
|
return new DdsVectorSearchResult(
|
|
|
|
|
"token", userLabel, properties.vectorObject(), normalizedQuery, mode,
|
|
|
|
|
embeddingModel(mode), true, "토큰 기반 DDS 권한으로 검색했습니다.", message,
|
|
|
|
|
sessionUser, endUser, null, rows);
|
|
|
|
|
sessionUser, endUser, null, rows, evidence);
|
|
|
|
|
}
|
|
|
|
|
} catch (SQLException exception) {
|
|
|
|
|
return failure("token", userLabel, normalizedQuery, mode,
|
|
|
|
|
@@ -251,7 +268,72 @@ public class DdsVectorKnowledgeService {
|
|
|
|
|
) {
|
|
|
|
|
return new DdsVectorSearchResult(
|
|
|
|
|
userKey, userLabel, properties.vectorObject(), query, mode, embeddingModel(mode),
|
|
|
|
|
false, title, message, null, null, oracleCode, List.of());
|
|
|
|
|
false, title, message, null, null, oracleCode, List.of(), null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private DdsSqlEvidence collectEvidence(Connection connection, String marker, String submittedStatement,
|
|
|
|
|
int timeoutSeconds) {
|
|
|
|
|
try (PreparedStatement statement = connection.prepareStatement("""
|
|
|
|
|
SELECT sql_id, child_number, TO_CHAR(last_active_time, 'YYYY-MM-DD HH24:MI:SS'), executions
|
|
|
|
|
FROM v$sql WHERE sql_text LIKE ? ORDER BY last_active_time DESC FETCH FIRST 1 ROW ONLY
|
|
|
|
|
""")) {
|
|
|
|
|
statement.setQueryTimeout(timeoutSeconds);
|
|
|
|
|
statement.setString(1, "%" + marker + "%");
|
|
|
|
|
try (ResultSet result = statement.executeQuery()) {
|
|
|
|
|
if (!result.next()) {
|
|
|
|
|
return DdsSqlEvidence.unavailable(marker, submittedStatement, "실행 SQL ID를 아직 찾지 못했습니다.");
|
|
|
|
|
}
|
|
|
|
|
String sqlId = result.getString(1);
|
|
|
|
|
int child = result.getInt(2);
|
|
|
|
|
String vpd = readPredicate(connection, "SELECT predicate FROM v$vpd_policy WHERE sql_id = ?", sqlId, timeoutSeconds);
|
|
|
|
|
String plan = readPlanPredicate(connection, sqlId, child, timeoutSeconds);
|
|
|
|
|
return new DdsSqlEvidence(true, marker, submittedStatement, sqlId, child, result.getString(3),
|
|
|
|
|
result.getLong(4), plan, vpd,
|
|
|
|
|
vpd != null ? "VPD 정책 predicate를 확인했습니다."
|
|
|
|
|
: "DDS DATA GRANT 경로의 실행 SQL을 확인했습니다.");
|
|
|
|
|
}
|
|
|
|
|
} catch (SQLException exception) {
|
|
|
|
|
return DdsSqlEvidence.unavailable(marker, submittedStatement,
|
|
|
|
|
"실행 근거 조회 권한이 없습니다. V$SQL, V$VPD_POLICY, DBMS_XPLAN 권한을 확인하세요.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String readPredicate(Connection connection, String sql, String sqlId, int timeoutSeconds) {
|
|
|
|
|
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
|
|
|
|
statement.setQueryTimeout(timeoutSeconds);
|
|
|
|
|
statement.setString(1, sqlId);
|
|
|
|
|
try (ResultSet result = statement.executeQuery()) {
|
|
|
|
|
return result.next() ? compact(result.getString(1)) : null;
|
|
|
|
|
}
|
|
|
|
|
} catch (SQLException ignored) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String readPlanPredicate(Connection connection, String sqlId, int child, int timeoutSeconds) {
|
|
|
|
|
StringBuilder output = new StringBuilder();
|
|
|
|
|
try (PreparedStatement statement = connection.prepareStatement(
|
|
|
|
|
"SELECT plan_table_output FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(?, ?, 'ALLSTATS LAST PREDICATE'))")) {
|
|
|
|
|
statement.setQueryTimeout(timeoutSeconds);
|
|
|
|
|
statement.setString(1, sqlId);
|
|
|
|
|
statement.setInt(2, child);
|
|
|
|
|
try (ResultSet result = statement.executeQuery()) {
|
|
|
|
|
boolean predicate = false;
|
|
|
|
|
while (result.next()) {
|
|
|
|
|
String line = result.getString(1);
|
|
|
|
|
if (line != null && line.contains("Predicate Information")) predicate = true;
|
|
|
|
|
if (predicate && line != null && output.length() < 1800) output.append(line.trim()).append('\n');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (SQLException ignored) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return compact(output.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String compact(String value) {
|
|
|
|
|
if (value == null || value.isBlank()) return null;
|
|
|
|
|
return value.replaceAll("\\s+", " ").trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String readSingleValue(Connection connection, String sql, int timeoutSeconds)
|
|
|
|
|
|