Compare commits
No commits in common. "f28a530e854d2fc70cb49dab288bd5e8cef12099" and "66e02868a6b886ff4470e2e53b2aa57e0702d4fd" have entirely different histories.
f28a530e85
...
66e02868a6
|
|
@ -559,10 +559,10 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
|
|||
"result": {
|
||||
"contents": [
|
||||
{
|
||||
"uri": "memory://recent",
|
||||
"mimeType": "application/json",
|
||||
"uri": "memory://recent",
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"message\": \"Resource content for memory://recent\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
|
||||
dnf update -y
|
||||
dnf -qy module disable postgresql
|
||||
|
||||
dnf install -y postgresql17-server
|
||||
|
||||
su - postgres
|
||||
/usr/pgsql-17/bin/initdb -D /var/lib/pgsql/17/data/
|
||||
exit
|
||||
|
||||
systemctl enable --now postgresql-17
|
||||
cd /tmp
|
||||
git clone --branch v0.8.1 https://github.com/pgvector/pgvector.git
|
||||
cd pgvector
|
||||
|
||||
dnf config-manager --set-enabled crb
|
||||
dnf install -y perl-IPC-Run
|
||||
dnf install -y git gcc make postgresql17-devel redhat-rpm-config
|
||||
|
||||
PATH=/usr/pgsql-17/bin:$PATH make
|
||||
PATH=/usr/pgsql-17/bin:$PATH make install
|
||||
|
||||
systemctl restart postgresql-17
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
-- ============================================
|
||||
-- Dewey 애플리케이션 데이터베이스 설정 스크립트
|
||||
-- ============================================
|
||||
-- 이 스크립트는 postgres superuser로 실행해야 합니다.
|
||||
-- 실행 방법: psql -U postgres -f database_setup.sql
|
||||
|
||||
-- 1. dewey 사용자 생성 (이미 존재하면 스킵)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = 'dewey') THEN
|
||||
CREATE USER dewey WITH PASSWORD '0bk1rWu98mGl5ea3';
|
||||
RAISE NOTICE 'dewey 사용자가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'dewey 사용자가 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. dewey_memory 데이터베이스 생성 (이미 존재하면 스킵)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'dewey_memory') THEN
|
||||
CREATE DATABASE dewey_memory OWNER dewey;
|
||||
RAISE NOTICE 'dewey_memory 데이터베이스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'dewey_memory 데이터베이스가 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. dewey_memory 데이터베이스에 연결
|
||||
\c dewey_memory
|
||||
|
||||
-- 3. pgvector 확장 설치 (superuser 권한 필요)
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- 4. dewey 사용자에게 필요한 최소 권한만 부여
|
||||
-- (보안을 위해 최소한의 권한만 부여)
|
||||
GRANT CONNECT ON DATABASE dewey_memory TO dewey;
|
||||
GRANT USAGE ON SCHEMA public TO dewey;
|
||||
GRANT CREATE ON SCHEMA public TO dewey;
|
||||
|
||||
-- 5. 기존 테이블/시퀀스에 대한 권한 부여
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO dewey;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO dewey;
|
||||
|
||||
-- 6. 향후 생성될 테이블/시퀀스에 대한 기본 권한 설정
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO dewey;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO dewey;
|
||||
|
||||
-- 7. dewey 사용자가 확장을 생성할 수 없도록 명시적으로 제한
|
||||
-- (확장 생성은 superuser만 가능하므로 이미 제한되어 있음)
|
||||
-- 하지만 혹시 모를 권한을 명시적으로 제거
|
||||
REVOKE CREATE ON DATABASE dewey_memory FROM dewey;
|
||||
|
||||
-- 8. dewey 사용자가 다른 사용자를 생성하거나 권한을 부여할 수 없도록 제한
|
||||
-- (이미 dewey는 일반 사용자이므로 불필요하지만 명시적으로 확인)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- dewey가 superuser인지 확인하고 제거 (혹시 모를 경우 대비)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_user WHERE usename = 'dewey' AND usesuper = true
|
||||
) THEN
|
||||
ALTER USER dewey WITH NOSUPERUSER;
|
||||
RAISE NOTICE 'dewey 사용자의 superuser 권한이 제거되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'dewey 사용자는 이미 일반 사용자입니다.';
|
||||
END IF;
|
||||
|
||||
-- dewey가 데이터베이스를 생성할 수 없도록 제한
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_user WHERE usename = 'dewey' AND usecreatedb = true
|
||||
) THEN
|
||||
ALTER USER dewey WITH NOCREATEDB;
|
||||
RAISE NOTICE 'dewey 사용자의 CREATEDB 권한이 제거되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'dewey 사용자는 이미 CREATEDB 권한이 없습니다.';
|
||||
END IF;
|
||||
|
||||
-- dewey가 다른 사용자를 생성할 수 없도록 제한
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_user WHERE usename = 'dewey' AND usecreaterole = true
|
||||
) THEN
|
||||
ALTER USER dewey WITH NOCREATEROLE;
|
||||
RAISE NOTICE 'dewey 사용자의 CREATEROLE 권한이 제거되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'dewey 사용자는 이미 CREATEROLE 권한이 없습니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 9. 권한 확인
|
||||
\echo '=== dewey 사용자 권한 확인 ==='
|
||||
\du dewey
|
||||
|
||||
\echo '=== public 스키마 권한 확인 ==='
|
||||
\dn+ public
|
||||
|
||||
\echo '=== pgvector 확장 확인 ==='
|
||||
SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';
|
||||
|
||||
\echo '=== 스크립트 실행 완료 ==='
|
||||
\echo 'dewey 사용자는 이제 최소한의 권한만 가지고 있습니다.'
|
||||
\echo '애플리케이션은 테이블/인덱스 생성 및 데이터 조작만 가능합니다.'
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ public class McpController {
|
|||
toolArguments = params.getArguments();
|
||||
}
|
||||
|
||||
yield mcpService.callTool(toolName, toolArguments);
|
||||
yield mcpService.callTool(toolName, toolArguments);
|
||||
}
|
||||
case "resources/list" -> mcpService.listResources();
|
||||
case "resources/read" -> {
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
package com.pandol365.dewey.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 데이터베이스 초기화
|
||||
* 프로젝트 시작 시 pgvector 확장 및 테이블 자동 생성
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(1) // 다른 초기화보다 먼저 실행
|
||||
@RequiredArgsConstructor
|
||||
public class DatabaseInitializer implements CommandLineRunner {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
try {
|
||||
log.info("=== 데이터베이스 초기화 시작 ===");
|
||||
|
||||
// 1. pgvector 확장 확인 (확장 생성은 시도만 하고, 실패해도 계속 진행)
|
||||
boolean pgvectorAvailable = checkPgVectorExtension();
|
||||
|
||||
if (!pgvectorAvailable) {
|
||||
log.warn("pgvector 확장이 설치되지 않았습니다. 벡터 검색 기능을 사용하려면 pgvector 확장을 설치해주세요.");
|
||||
log.warn("설치 방법: PostgreSQL 서버에서 다음 명령어 실행:");
|
||||
log.warn(" psql -U postgres -d dewey_memory -c 'CREATE EXTENSION vector;'");
|
||||
log.warn("또는 dewey 사용자에게 확장 생성 권한 부여 후 애플리케이션 재시작");
|
||||
log.info("=== 데이터베이스 초기화 완료 (pgvector 없음) ===");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. memory_embeddings 테이블 생성
|
||||
createMemoryEmbeddingsTable();
|
||||
|
||||
// 3. 인덱스 생성
|
||||
createVectorIndex();
|
||||
|
||||
log.info("=== 데이터베이스 초기화 완료 ===");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("데이터베이스 초기화 실패: {}", e.getMessage(), e);
|
||||
// 초기화 실패해도 애플리케이션은 계속 실행 (이미 존재하는 경우 등)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pgvector 확장 확인
|
||||
* @return 확장이 사용 가능하면 true
|
||||
*/
|
||||
private boolean checkPgVectorExtension() {
|
||||
try {
|
||||
log.info("pgvector 확장 확인 중...");
|
||||
|
||||
// 현재 데이터베이스에서 확장이 이미 존재하는지 확인
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM pg_extension WHERE extname = 'vector'",
|
||||
Integer.class
|
||||
);
|
||||
|
||||
if (count != null && count > 0) {
|
||||
log.info("pgvector 확장이 이미 설치되어 있습니다.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 확장이 없으면 생성 시도
|
||||
log.info("pgvector 확장 생성 시도 중...");
|
||||
try {
|
||||
jdbcTemplate.execute("CREATE EXTENSION IF NOT EXISTS vector");
|
||||
|
||||
// 생성 후 다시 확인
|
||||
count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM pg_extension WHERE extname = 'vector'",
|
||||
Integer.class
|
||||
);
|
||||
|
||||
if (count != null && count > 0) {
|
||||
log.info("pgvector 확장 생성 완료");
|
||||
return true;
|
||||
} else {
|
||||
log.warn("pgvector 확장 생성 후에도 확인되지 않습니다.");
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 확장이 이미 존재하는 경우도 있으므로 다시 확인
|
||||
log.debug("CREATE EXTENSION 실패, 재확인 중: {}", e.getMessage());
|
||||
count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM pg_extension WHERE extname = 'vector'",
|
||||
Integer.class
|
||||
);
|
||||
|
||||
if (count != null && count > 0) {
|
||||
log.info("pgvector 확장이 존재합니다 (생성 시도 중 오류 발생했지만 확장은 존재함)");
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn("pgvector 확장 생성 실패: {}", e.getMessage());
|
||||
log.warn("현재 데이터베이스에 pgvector 확장이 없습니다. 다음 명령어를 실행해주세요:");
|
||||
log.warn(" psql -d dewey_memory -c 'CREATE EXTENSION vector;'");
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("pgvector 확장 확인 중 오류 발생: {}", e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* memory_embeddings 테이블 생성
|
||||
*/
|
||||
private void createMemoryEmbeddingsTable() {
|
||||
try {
|
||||
log.info("memory_embeddings 테이블 생성 중...");
|
||||
String sql = """
|
||||
CREATE TABLE IF NOT EXISTS memory_embeddings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
memory_id BIGINT NULL,
|
||||
user_id VARCHAR(255),
|
||||
memory_text TEXT NOT NULL,
|
||||
importance INT DEFAULT 1,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
embedding VECTOR(768) NOT NULL
|
||||
)
|
||||
""";
|
||||
jdbcTemplate.execute(sql);
|
||||
log.info("memory_embeddings 테이블 생성 완료");
|
||||
} catch (Exception e) {
|
||||
log.warn("memory_embeddings 테이블 생성 실패 (이미 존재할 수 있음): {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 벡터 인덱스 생성 (cosine similarity)
|
||||
*/
|
||||
private void createVectorIndex() {
|
||||
try {
|
||||
log.info("벡터 인덱스 생성 중...");
|
||||
// 기존 인덱스가 있으면 삭제 후 재생성 (IF NOT EXISTS는 ivfflat에서 지원 안 함)
|
||||
String dropIndexSql = "DROP INDEX IF EXISTS memory_embeddings_embedding_idx";
|
||||
jdbcTemplate.execute(dropIndexSql);
|
||||
|
||||
// ivfflat 인덱스 생성 (lists=100은 데이터량에 따라 조정 필요)
|
||||
String createIndexSql = """
|
||||
CREATE INDEX memory_embeddings_embedding_idx
|
||||
ON memory_embeddings USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 100)
|
||||
""";
|
||||
jdbcTemplate.execute(createIndexSql);
|
||||
log.info("벡터 인덱스 생성 완료");
|
||||
} catch (Exception e) {
|
||||
log.warn("벡터 인덱스 생성 실패 (이미 존재할 수 있음): {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.pandol365.dewey.config;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class EmbeddingConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
package com.pandol365.dewey.domain.memory.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 임베딩 생성 클라이언트
|
||||
* all-mpnet-base-v2 (768d) 같은 로컬/외부 서비스에 HTTP 호출
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EmbeddingClient {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${embedding.api.base-url:http://localhost:8000}")
|
||||
private String embeddingBaseUrl;
|
||||
|
||||
@Value("${embedding.api.path:/embeddings}")
|
||||
private String embeddingPath;
|
||||
|
||||
@Value("${embedding.api.model:sentence-transformers/all-mpnet-base-v2}")
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 텍스트 임베딩 생성
|
||||
* Infinity API 형식: 요청 { "model": "...", "input": "..." }, 응답 { "data": [{"embedding": [...]}] }
|
||||
*/
|
||||
public float[] embed(String text) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.newInstance()
|
||||
.scheme(extractScheme(embeddingBaseUrl))
|
||||
.host(extractHost(embeddingBaseUrl))
|
||||
.port(extractPort(embeddingBaseUrl))
|
||||
.path(embeddingPath == null ? "/embeddings" : embeddingPath)
|
||||
.toUriString();
|
||||
|
||||
// Infinity API 요청 형식: {"model": "...", "input": "..."}
|
||||
Map<String, Object> request = Map.of(
|
||||
"model", embeddingModel != null ? embeddingModel : "sentence-transformers/all-mpnet-base-v2",
|
||||
"input", text
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> resp = restTemplate.postForObject(
|
||||
url,
|
||||
request,
|
||||
Map.class
|
||||
);
|
||||
|
||||
if (resp == null || !resp.containsKey("data")) {
|
||||
throw new IllegalStateException("data field missing in response");
|
||||
}
|
||||
|
||||
Object dataObj = resp.get("data");
|
||||
if (!(dataObj instanceof List<?> dataList) || dataList.isEmpty()) {
|
||||
throw new IllegalStateException("data is not a non-empty list");
|
||||
}
|
||||
|
||||
// data[0].embedding 추출
|
||||
Object firstItem = dataList.get(0);
|
||||
if (!(firstItem instanceof Map<?, ?> firstMap)) {
|
||||
throw new IllegalStateException("data[0] is not a map");
|
||||
}
|
||||
|
||||
Object embObj = firstMap.get("embedding");
|
||||
if (!(embObj instanceof List<?> list)) {
|
||||
throw new IllegalStateException("embedding is not a list");
|
||||
}
|
||||
|
||||
float[] embedding = new float[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Object v = list.get(i);
|
||||
if (v instanceof Number num) {
|
||||
embedding[i] = num.floatValue();
|
||||
} else {
|
||||
throw new IllegalStateException("embedding element is not numeric");
|
||||
}
|
||||
}
|
||||
return embedding;
|
||||
} catch (Exception e) {
|
||||
log.error("임베딩 생성 실패: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to generate embedding", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 간단한 URL 파서 (기본값 포함)
|
||||
private String extractScheme(String url) {
|
||||
if (url == null || url.isBlank()) return "http";
|
||||
int idx = url.indexOf("://");
|
||||
return idx > 0 ? url.substring(0, idx) : "http";
|
||||
}
|
||||
|
||||
private String extractHost(String url) {
|
||||
if (url == null || url.isBlank()) return "localhost";
|
||||
String noScheme = url.contains("://") ? url.substring(url.indexOf("://") + 3) : url;
|
||||
int slash = noScheme.indexOf('/');
|
||||
String hostPort = slash >= 0 ? noScheme.substring(0, slash) : noScheme;
|
||||
int colon = hostPort.indexOf(':');
|
||||
return colon >= 0 ? hostPort.substring(0, colon) : hostPort;
|
||||
}
|
||||
|
||||
private Integer extractPort(String url) {
|
||||
if (url == null || url.isBlank()) return null;
|
||||
String noScheme = url.contains("://") ? url.substring(url.indexOf("://") + 3) : url;
|
||||
int slash = noScheme.indexOf('/');
|
||||
String hostPort = slash >= 0 ? noScheme.substring(0, slash) : noScheme;
|
||||
int colon = hostPort.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
String portStr = hostPort.substring(colon + 1);
|
||||
try {
|
||||
return Integer.parseInt(portStr);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
package com.pandol365.dewey.domain.memory.service;
|
||||
|
||||
import com.pandol365.dewey.domain.memory.model.Memory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.pgvector.PGvector;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PGVector 기반 메모리 벡터 스토어
|
||||
* 별도 테이블 memory_embeddings 에 저장/검색
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MemoryVectorStore {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final String TABLE_NAME = "memory_embeddings";
|
||||
|
||||
/**
|
||||
* 임베딩과 함께 저장 (permanent 목적)
|
||||
*/
|
||||
public void save(String userId, String memoryText, Integer importance, float[] embedding) {
|
||||
try {
|
||||
PGvector pgVector = new PGvector(embedding);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO " + TABLE_NAME + " (user_id, memory_text, importance, created_at, embedding) VALUES (?, ?, ?, ?, ?)",
|
||||
userId,
|
||||
memoryText,
|
||||
importance != null ? importance : 1,
|
||||
LocalDateTime.now(),
|
||||
pgVector
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("memory_embeddings 저장 실패: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to store embedding", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 코사인 유사도 기반 검색
|
||||
*/
|
||||
public List<Memory> search(float[] queryEmbedding, int limit) {
|
||||
try {
|
||||
PGvector pgVector = new PGvector(queryEmbedding);
|
||||
String sql = "SELECT id, user_id, memory_text, importance, created_at, " +
|
||||
" (embedding <=> ?) AS distance " +
|
||||
"FROM " + TABLE_NAME + " " +
|
||||
"ORDER BY embedding <=> ? " +
|
||||
"LIMIT ?";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||
Memory m = new Memory();
|
||||
m.setId(rs.getLong("id"));
|
||||
m.setUserId(rs.getString("user_id"));
|
||||
m.setMemoryText(rs.getString("memory_text"));
|
||||
m.setImportance(rs.getInt("importance"));
|
||||
m.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
|
||||
m.setUpdatedAt(null);
|
||||
return m;
|
||||
}, pgVector, pgVector, limit);
|
||||
} catch (Exception e) {
|
||||
log.error("memory_embeddings 검색 실패: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to search embeddings", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,8 +4,6 @@ import com.pandol365.dewey.domain.memory.model.Memory;
|
|||
import com.pandol365.dewey.domain.memory.model.TemporaryMemory;
|
||||
import com.pandol365.dewey.domain.memory.repository.MemoryRepository;
|
||||
import com.pandol365.dewey.domain.memory.service.MemoryService;
|
||||
import com.pandol365.dewey.domain.memory.service.EmbeddingClient;
|
||||
import com.pandol365.dewey.domain.memory.service.MemoryVectorStore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
|
@ -31,8 +29,6 @@ public class MemoryServiceImpl implements MemoryService {
|
|||
|
||||
private final MemoryRepository memoryRepository;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final EmbeddingClient embeddingClient;
|
||||
private final MemoryVectorStore memoryVectorStore;
|
||||
|
||||
private static final String TEMP_MEMORY_KEY_PREFIX = "tempMemory:";
|
||||
private static final String USER_TEMP_MEMORY_KEY_PREFIX = "user:tempMemories:";
|
||||
|
|
@ -63,14 +59,6 @@ public class MemoryServiceImpl implements MemoryService {
|
|||
redisTemplate.opsForZSet().add(userMemoriesKey, memoryKey, System.currentTimeMillis());
|
||||
redisTemplate.expire(userMemoriesKey, ttl);
|
||||
|
||||
// 임베딩 생성 및 PGVector 저장 (실패해도 Redis 저장은 유지)
|
||||
try {
|
||||
float[] embedding = embeddingClient.embed(memoryText);
|
||||
memoryVectorStore.save(userId, memoryText, importance, embedding);
|
||||
} catch (Exception e) {
|
||||
log.warn("임베딩 저장 실패 (계속 진행): {}", e.getMessage());
|
||||
}
|
||||
|
||||
return temporaryMemory;
|
||||
}
|
||||
|
||||
|
|
@ -121,16 +109,10 @@ public class MemoryServiceImpl implements MemoryService {
|
|||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Memory> searchMemoriesByVector(String query, String userId, Integer limit) {
|
||||
log.info("벡터 기반 메모리 검색: query={}, userId(optional)={}, limit={}", query, userId, limit);
|
||||
log.info("벡터 기반 메모리 검색: query={}, userId={}, limit={}", query, userId, limit);
|
||||
|
||||
int topK = limit != null && limit > 0 ? limit : 5;
|
||||
try {
|
||||
float[] embedding = embeddingClient.embed(query);
|
||||
return memoryVectorStore.search(embedding, topK);
|
||||
} catch (Exception e) {
|
||||
log.error("벡터 검색 실패: {}", e.getMessage(), e);
|
||||
return List.of();
|
||||
}
|
||||
// TODO: 벡터 유사도 검색 구현 (Spring AI PgVectorStore 사용)
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
spring.application.name=dewey
|
||||
|
||||
# PostgreSQL
|
||||
spring.datasource.url=jdbc:postgresql://192.168.100.230:5432/dewey_memory
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/dewey_memory
|
||||
spring.datasource.username=dewey
|
||||
spring.datasource.password=0bk1rWu98mGl5ea3
|
||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
|
|
@ -15,9 +15,3 @@ spring.jpa.properties.hibernate.format_sql=true
|
|||
# Redis
|
||||
spring.data.redis.host=localhost
|
||||
spring.data.redis.port=6379
|
||||
|
||||
# Embedding service
|
||||
# all-mpnet-base-v2 768d embedding endpoint (infinity)
|
||||
embedding.api.base-url=http://192.168.100.220:8000
|
||||
embedding.api.path=/embeddings
|
||||
embedding.api.model=sentence-transformers/all-mpnet-base-v2
|
||||
|
|
|
|||
Loading…
Reference in New Issue