
【무료】로컬 LLM으로 벡터 DB 사용해 보기
요약
로컬 환경에서 Oracle 26ai를 활용하여 벡터 DB를 구축하고 사용하는 방법을 다룹니다. LlamaIndex 같은 프레임워크 없이 직접 비구조화 데이터를 벡터 좌표로 변환하여 테이블에 저장하는 구조적 원리를 학습합니다.
핵심 포인트
- Oracle 26ai를 활용한 로컬 벡터 DB 구축 방법 안내
- 비구조화 데이터를 벡터 좌표로 변환하여 저장하는 원리 설명
- RAG와 벡터 DB의 구조적 차이 및 관계 이해
- Python과 SQL을 이용한 벡터 데이터 삽입 프로세스
-
Windows 11 Home 25H2
-
RAM 16.0GB
-
NVIDIA GeForce RTX 3050 Ti Laptop GPU (4 GB)
-
Oracle 26ai free
-
Python
-
Python 초보자
-
AI 초보자
-
평소 Oracle을 사용하고 있는 사람
AI 학습의 시작으로서, 무료이며 로컬에서 부담 없이 시도해 볼 수 있는 환경에서 동작 확인을 진행합니다.
프로세스를 확인하면서 구조를 학습하는 것이 목적이므로, LlamaIndex와 같은 프레임워크는 이번에 사용하지 않습니다.
문장이나 이미지 등의 비구조화 데이터 (Unstructured Data)를 벡터 좌표로 나타내어 테이블의 레코드에 투입한 것입니다.
좌표적으로 가까운 것을 의미가 가까운 것으로 취득할 수 있게 됩니다.
RAG와 세트로 설명되는 경우가 많지만, RAG는 외부에서 데이터를 가져와 LLM에 전달하는 구조이므로 벡터 DB가 필수인 것은 아닙니다.
LLM도 벡터 표현 (Vector Representation)을 사용하여 학습·추론하므로 구조적으로는 유사하지만, 직접 데이터를 보유하고 있는 것이 아니라 학습된 파라미터 (Parameters)로서 보유하고 있습니다.
LLM 내부에는 컨텍스트 윈도우 (Context Window)라는 메모리 영역이 있지만, 대량의 데이터를 보유할 수 있는 것은 아닙니다.
기존의 시스템 구성에 비유하자면, LLM은 AP 서버, 벡터 DB는 DB 서버, RAG는 AP 서버에서 SQL을 발행하여 DB 서버에 질의하는 아키텍처와 같습니다.
공식 사이트에서 Windows 버전을 다운로드하여 설치합니다.
초기 상태에서는 listener.ora나 tnsnames.ora의 IP 주소가 프라이빗 IP 주소가 되므로, 127.0.0.1로 수정하고 재시작합니다.
sys as sysdba로 접속하여
CREATE TABLE test_vector (
id NUMBER,
embedding VECTOR(3, FLOAT32)
...
이 통과되면 벡터 DB를 사용할 수 있습니다.
다음으로, PDB의 명칭(아마도 FREEPDB1)을 확인하고 open 되어 있지 않다면 open 합니다.
SHOW PDBS
ALTER PLUGGABLE DATABASE FREEPDB1 OPEN;
ALTER SESSION SET CONTAINER = FREEPDB1;
...
PDB는 TNS 접속만 허용하므로 tnsnames.ora에 추가합니다.
RAG =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521))
...
PDB에 SYSTEM으로 접속하여 비밀번호 만료를 방지한 후 사용자를 생성합니다.
우선 사용자 이름과 비밀번호는 rag로 설정해 두겠습니다.
ALTER PROFILE DEFAULT LIMIT PASSWORD_LIFE_TIME UNLIMITED;
ALTER PROFILE DEFAULT LIMIT FAILED_LOGIN_ATTEMPTS UNLIMITED;
ALTER PROFILE DEFAULT LIMIT PASSWORD_LOCK_TIME UNLIMITED;
...
rag 사용자로 접속하여 벡터 DB 테이블을 생성해 보겠습니다.
참고로 VECTOR 타입은 내부적으로 SecureFiles LOB를 사용하고 있으며, 이는 ASSM (자동 세그먼트 관리, Automatic Segment Management) 환경이 아니면 생성할 수 없습니다.
CREATE TABLE documents (
id NUMBER GENERATED ALWAYS AS IDENTITY,
content CLOB,
...
Python이 없다면 설치합니다.
IDE나 라이브러리도 필요에 따라 설치합니다.
Oracle의 에러 메시지 "ORA-01555 snapshot too old"를 벡터로 변환하고, 리스트 (List)로 만든 뒤, 그것을 JSON 문자열로 변환하면 TO_VECTOR()로 INSERT 할 수 있습니다.
벡터 변환에는 이번에 임베딩 (Embedding) 모델인 "multilingual-e5-small"을 사용합니다.
일본어 대응이 가능하며 무료이고, 처리 능력은 낮지만 고정밀이라고 합니다.
실행 시 WARNING이 발생하지만, 이는 Hugging Face라는 회사로부터 익명으로 다운로드하고 있기 때문에, 속도 제한이나 다운로드 속도 저하 가능성이 발생하는 것에 대한 경고이므로 테스트 시에는 무시해도 괜찮습니다.
제거하려면 무료 계정을 생성하여 로그인하거나 토큰을 지정해야 합니다.
이후 라이브러리 임포트(import)는 생략합니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
위에서 INSERT한 것을 SELECT합니다.
CONTENT는 CLOB 타입이므로, READ해야 합니다.
여기서는 「UNDO 부족」이라는 문장을 입력하면, 이를 벡터(vector) 변환하여 documents 내의 레코드와 비교하고 코사인 유사도(cosine similarity, 벡터적인 각도나 방향이 가까운 정도)가 높은 것을 출력합니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
파일에서 읽어올 경우에는 텍스트 파일을 다음과 같이 준비합니다.
빈 줄로 구분하여 하나의 청크(chunk, 덩어리)로 만듭니다.
청크 단위로 벡터 변환을 수행하므로, 의미적으로 묶여 있도록 구성하는 것이 좋습니다.
여기서는 1 에러 = 1 청크 = 1 레코드로 설정했습니다.
ORA-01555 snapshot too old
UNDO 부족이나 장시간 트랜잭션(transaction)으로 발생
ORA-01652 unable to extend temp segment
...
Python의 INSERT는 다음과 같습니다. 단순히 루프(loop) 처리로 만든 것뿐입니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
나아가 ORA 에러를 공식 사이트(영어)에서 어느 정도 수집하여 ora_errors.txt에 붙여넣습니다. (수동으로 복사/붙여넣기)
INSERT 후 「UNDO 부족의 대처법」으로 SELECT하면 다음과 같은 출력을 얻을 수 있습니다.
ORA-01555
snapshot too old: rollback segment number string with name "string" too small
Cause
...
보시는 바와 같이, 내용은 에러 코드, 개요, 에러의 원인, 대처법에 대해 기재되어 있습니다.
대처법에 대해 물어도 1 에러 = 1 청크 = 1 레코드이기 때문에 전체 문장을 출력하는 것밖에 할 수 없습니다.
에러 코드 + 개요, 원인, 대처법으로 각각 청크를 나누어 봅시다. 다음과 같이 빈 줄로 구분하기만 하면 됩니다.
ORA-01555
snapshot too old: rollback segment number string with name "string" too small
Cause
...
ORA-로 시작하는 CONTENT는 요약,
CAUSE로 시작하는 CONTENT는 원인,
ACTION으로 시작하는 CONTENT는 대처로 카테고리화(categorize)할 수 있습니다.
메타데이터(metadata)로서 CATEGORY 열을 추가합니다.
DROP TABLE documents;
CREATE TABLE documents (
id NUMBER GENERATED ALWAYS AS IDENTITY,
...
이번에는 투박하게 정규 표현식(regular expression)으로 처리하겠습니다. LLM에 전달하여 JSON 문자열을 반환하게 하는 방법도 있지만, 그것은 나중에 하겠습니다.
re.split을 사용하여 파일 전체의 문자열 text를 분할하여 배열 blocks에 저장합니다.
에러 단위로 분할하기 위해 「ORA-xxxxx」를 기준으로 분할합니다.
re.S는 re.DOTALL의 별칭으로, 정규 표현식의 마침표(.)가 줄바꿈 문자를 포함한 모든 문자에 매치되도록 합니다.
import oracledb
import json
import re
...
검색 처리는 다음과 같습니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
출력은 다음과 같습니다.
쿼리가 「UNDO 부족의 대처법」일 때, 상위에 ACTION 카테고리가 나오고 있음을 확인할 수 있습니다.
ORA-01555
ACTION
If in Automatic Undo Management mode, increase undo_retention setting. Otherwise, use larger rollback segments
...
1 에러 = 1 청크(Chunk)인 경우, 원인이나 대처법에 대해 질문해도 답변(content)은 청크의 전체 내용이 출력되지만, 청크를 나눔으로써 원인에 대해 답변하게 하거나 대처법에 대해 답변하게 할 수 있습니다.
SQL 문의 WHERE 절에 category='Cause' 등을 넣어 필터링하는 것도 가능합니다.
키워드 검색과 벡터 검색을 결합하는 것을 일반적으로 하이브리드 검색(Hybrid Search)이라고 합니다.
벡터 DB를 어느 정도 다루게 되었다면, 로컬 LLM과 연동해 봅시다.
먼저 ollama(로컬 환경에서 LLM을 실행할 수 있는 도구)를 설치합니다.
LLM에 대해서는 이번에 gemma(Google의 무료 LLM)를 사용하고 있지만, PC의 GPU나 메모리에 따라 다르므로 어떤 것을 사용할지는 확인해 보시기 바랍니다.
다운로드 및 설정 후,
ollama pull gemma3:4b
로 설치할 수 있으며,
ollama run gemma3:4b
로 실행합니다.
Python으로 동작 확인을 해보겠습니다.
from ollama import chat
response = chat(
model="gemma3:4b",
...
다음과 같은 답변이 나옵니다.
Oracle의 UNDO는 데이터베이스 트랜잭션이 커밋(Commit)되기 전의 아주 미세한 변경 사항을 유지하여, 롤백(Rollback)이나 복구 시에 사용하기 위한 메커니즘입니다.
다음은 Oracle의 UNDO에 관한 중요한 포인트를 정리한 것입니다.
**1. 왜 UNDO가 필요한가?**
...
벡터 DB의 검색 프로세스에 도입해 보겠습니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
출력은 다음과 같습니다.
LLM으로부터의 답변은 마지막 일본어 한 줄입니다.
=== CONTEXT ===
ERROR_CODE:ORA-01555
CATEGORY:ACTION
...
조정을 해보겠습니다.
CATEGORY를 CAUSE와 ACTION으로 설정하여 30행을 가져오고, 프롬프트(Prompt)에서는 원인과 대처법을 알려주도록 지정하며, DISTANCE는 0.3 미만(거리상 가까운 것만 가져오도록)으로 설정해 보겠습니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
다음과 같은 답변이 나옵니다. 이전 답변보다 정밀도가 높아진 것 같습니다.
UNDO 부족으로 업데이트 처리가 실패한 원인과 대처법은 다음과 같습니다.
**원인:**
* **ERROR_CODE:ORA-01555**
...
참고로 여기서는 처음에 "UNDO 부족의 대처법"으로 쿼리를 DB에 던지고, 그 결과를 "UNDO 부족으로 업데이트 처리가 실패했다"라는 질문의 정보 소스(Information Source)로 사용하고 있습니다.
이는 UNDO 부족이라는 질문이 올 것을 상정하고 있지만, 실제로 UNDO 부족에 관한 질문이 올지는 알 수 없습니다.
먼저 질문을 한 뒤에 쿼리를 DB에 던지는 것이 본래의 형태입니다.
예를 들어 "UNDO 부족의 원인은?"이라고 물으면 WHERE 절에 CAUSE를, "UNDO 부족의 대처법은?"이라고 물으면 WHERE 절에 ACTION을 붙이는 식으로 동적으로 변경하게 합니다.
단순한 구현으로는 특정 문구나 번호를 사용자에게 입력하게 할 수도 있고, LLM 스스로 판단하게 하는 방법도 있습니다.
현재는 벡터 DB의 모든 데이터를 거리 계산하고 있습니다.
벡터 인덱스(HNSW)를 만들어 봅시다.
간단히 말하면, 가까운 벡터끼리 연결하여 지름길을 만들 수 있도록 하는 것입니다.
그전에 Oracle의 초기화 파라미터를 변경하여 메모리를 확보해야 합니다.
CDB에 접속하여(VECTOR_MEMORY_SIZE는 CDB의 파라미터입니다)
파라미터를 변경하고 재시작합니다.
ALTER SYSTEM SET VECTOR_MEMORY_SIZE=200M SCOPE=SPFILE;
SHUTDOWN IMMEDIATE;
STARTUP;
PDB에 접속하여 인덱스를 생성합니다.
CREATE VECTOR INDEX documents_hnsw_idx
ON documents(embedding)
ORGANIZATION INMEMORY NEIGHBOR GRAPH
...
실행 계획을 확인합니다.
VECTOR INDEX HNSW SCAN으로 되어 있다면 OK입니다.
TO_VECTOR의 내용은 이번에 직접 만들었습니다. Oracle이 제공하는 모델을 사용함으로써 DB 내부에서 임베딩 (embedding)을 하는 것도 가능해 보이지만, free 버전에서는 사용할 수 없는 것 같습니다.
EXPLAIN PLAN FOR
SELECT *
FROM documents
...
Oracle 26ai에서는 HNSW 이외에도 IVF라는 인덱스 (index)를 지원합니다.
간단히 말하면 클러스터 (cluster)로 나누어, 유사한 클러스터만 검색하는 방식입니다.
Oracle 관점에서는 인덱스 (index)라기보다 파티션 (partition)에 가까울지도 모릅니다.
HNSW는 십만~수천만 건 규모에서 정밀도 중시,
IVF는 수천만~수억 건 규모에서 확장성 (scale) 중시입니다.
지금까지 텍스트 파일을 가져왔지만, 사내 정보를 가져와 데이터화할 경우에는 텍스트뿐만이 아니기 때문에, 시험 삼아 PDF를 가져와 보겠습니다.
샘플로서, 사전에 Oracle Database Error Messages (PDF)를 다운로드해 둡니다.
테이블도 만들어 둡니다.
CREATE TABLE pdf_documents (
id NUMBER GENERATED ALWAYS AS IDENTITY,
source_file VARCHAR2(200),
...
PDF를 읽어서 INSERT 합니다.
이번에는 ORA- 에러만 취득하므로 페이지를 특정하고 있습니다.
2000페이지 이상 있기 때문에, INSERT를 한 줄씩 루프 (loop) 돌리면 시간이 걸립니다.
executemany로 한꺼번에 실행하면 10분 정도면 끝납니다.
import oracledb
import json
import re
...
질문을 던져 보겠습니다.
지금까지 질문 내용은 코드 내에 직접 작성했지만, 실행 시 입력하도록 하고 있습니다.
import oracledb
import json
from sentence_transformers import SentenceTransformer
...
출력은 다음과 같습니다.
==================================================
Oracle의 에러에 대해 질문하고 싶은 내용을 입력해 주세요.
==================================================
...
Oracle Database Error Messages (PDF)와 같이 구조화된 기술 문서에서는 정규 표현식 (regular expression)을 통한 취득도 간단하지만, 예를 들어 업무 문서에서는 지사나 부서마다 포맷 (format)이나 키워드 (keyword)가 다를 수 있습니다.
앞부분에서도 살짝 언급했지만, 그럴 때는 LLM에게 JSON 문자열 등으로 정형화하도록 시킬 수도 있습니다.
다만 시간이 꽤 걸리는 모양인지, 아래는 2페이지뿐이지만 5분 정도 걸렸습니다.
from pypdf import PdfReader
from ollama import chat
reader = PdfReader("pdf\database-error-messages.pdf")
...
출력은 다음과 같습니다.
[
{
"error_code": "ORA-00000",
...
지금까지 Oracle의 에러 메시지만 다루어 왔지만, 일본어 장문을 가져와 요약 (summary) 시켜보고 싶습니다.
아오조라 문고 (Aozora Bunko)에서 적당한 장편 작품을 다운로드해 옵니다.
이번에는 러브크래프트의 「광기의 산맥에서」를 채택합니다. 주석 등 불필요한 부분은 수동으로 삭제해 둡니다.
청크 크기 (chunk size)에 대해서는 너무 커도 너무 작아도 성능이 떨어지기 때문에 어려운 부분입니다.
장(章) 단위나 개행 단위로는 잘 되지 않았습니다.
장문의 요약에 관해서는, 처음에 어느 정도 세밀하게 분할하고, 각 청크 (chunk)를 요약한 뒤, 그 요약된 것들을 몇 개씩 모아서 문자열 결합을 하여 다시 요약하는... 방식을 반복하면 잘 된다고 합니다.
이번에는 중간 테이블을 만들어 내용을 확인하면서 진행하겠습니다.
DROP TABLE text_documents;
CREATE TABLE text_documents (
id NUMBER GENERATED ALWAYS AS IDENTITY,
...
2000자마다 청크 (chunk)를 나누어 text_documents에 투입합니다.
Aozora Bunko는 1행에 제목, 2행에 저자명을 적는 경우가 많기 때문에 lines[0].strip()과 lines[1].strip()으로 가져오고 나머지를 본문으로 처리하고 있습니다. 하지만 그렇지 않은 경우도 있어서, "냐를라토텝"의 저자가 "NYARLATHOTEP"이 되거나 "다곤"의 저자가 "DAGON"이 되기도 하지만, 이번에는 신경 쓰지 않기로 하겠습니다.
import glob
import os
from sentence_transformers import SentenceTransformer
...
text_documents에 투입한 청크(chunk) 중, "광기의 산맥에서"의 content(본문)를 SELECT합니다.
CLOB 타입이므로 문자열로 만들기 위해 read()가 필요합니다.
원래는 2000자이지만, 이를 300자 이내로 요약하게 합니다.
1시간 가까이 걸리기 때문에 10건마다 book_chunk_summaries에 INSERT, COMMIT하며 진행 상황도 표시하고 있습니다.
대상은 55개 청크이므로, 남은 5개 청크를 마지막에 INSERT하고 있습니다.
from sentence_transformers import SentenceTransformer
from ollama import chat
import oracledb
...
요약 내용을 확인해 두겠습니다.
select summary from book_chunk_summaries
order by chunk_no
;
언뜻 보기에는 대체로 맞는 것 같습니다.
"남극으로의 대규모 탐사 계획에 반대하는 과학자들은 그 비현실적인 야심과 조작 가능성을 지적하며 신중한 자세를 보이고 있습니다.
그들은 지금까지 얻은 사진이나 항공 사진이 교묘한 원거리 촬영일 가능성을 의심하며, 기존의 조사 방법에 의문을 제기하고 있습니다.
지질학자들은 새로운 굴착 장치(Peabody형 드릴)를 이용한 탐사를 통해 선캄브리아기 지층으로부터 대량의 화석을 포함한 암석 샘플을 얻는 것을 목표로 하고 있었습니다.
...
book_chunk_summaries의 55개 청크를 10개씩 묶어서 문자열 결합을 하고, 300자 이내로 요약합니다.
이렇게 하면 300자 이내의 요약이 6개가 되므로, 마지막에 이것들을 모두 문자열 결합하여 요약합니다.
from sentence_transformers import SentenceTransformer
from ollama import chat
import oracledb
...
출력은 다음과 같습니다.
해백합상 생물은 "고대의 존재"이지만 마도서 네크로노미콘이 되어 있는 등, 약간의 환각 (Hallucination)을 일으키고 있는 것 같습니다.
재요약:
남극으로의 대규모 탐사 계획은 비현실적인 야심과 조작 가능성을 안고 있었다.
지질학자들은 선캄브리아기 화석 탐색 및 초기 생물사 해명을 목표로 했으나, 비주류 연구자들에 의한 활동이었기에 여론 영향력이 제한적이었다.
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기