벡터 검색이란 무엇인가 — MariaDB 11.8 VECTOR 타입과 Laravel 13 whereVectorSimilarTo 실측

  • 23rd September 2026
  • 6 min read

사내 문서 검색창에 "연차 어떻게 내요?" 라고 치면 아무것도 안 나온다. 문서 제목은 "휴가 결재 절차" 다. LIKE '%연차%' 로는 "휴가" 를 못 잡고, 전문검색(FULLTEXT)을 붙여도 단어가 다르면 마찬가지다. 사람은 두 문장이 같은 얘기라는 걸 바로 아는데 DB 는 글자가 다르니 남이다.

이 간격을 메우는 것이 벡터 검색이다. 그동안은 별도 벡터 DB 를 세우거나 PostgreSQL 에 pgvector 를 붙여야 했는데, MariaDB 11.7 부터 VECTOR 타입과 거리 함수·인덱스가 본체에 들어왔고, Laravel 13 의 쿼리 빌더가 이걸 whereVectorSimilarTo() 한 줄로 감싼다. 이 글은 벡터 검색이 무엇이고 어디에 쓰는지부터 시작해서, MariaDB 11.8 과 Laravel 13.33 에서 실제로 어떤 SQL 이 나가고 무엇이 돌아오는지 재 본 기록이다.

벡터 검색이란 — 문장을 좌표로 바꿔서 가까운 것을 찾는다

핵심은 두 단계다.

① 문장을 숫자 배열로 바꾼다. 이걸 임베딩(embedding)이라 부른다. 임베딩 모델(OpenAI 의 text-embedding, 구글·네이버의 것, 오픈소스 모델 등)에 문장을 넣으면 [0.021, -0.113, 0.087, …] 같은 실수 배열이 나온다. 길이는 모델마다 다른데 보통 768~3072 개다. 중요한 성질은 하나 — 뜻이 비슷한 문장은 비슷한 배열이 나온다. "연차 어떻게 내요?" 와 "휴가 결재 절차" 는 글자는 하나도 안 겹치지만 배열은 가깝게 나온다.

② 배열끼리 거리를 잰다. 배열을 고차원 공간의 점(벡터)으로 보면, 뜻이 비슷한 문장은 가까운 점이다. 검색이란 "질문 문장의 점에서 가장 가까운 문서 점 몇 개" 를 찾는 일이 된다. 거리는 주로 코사인 거리를 쓰는데, 두 벡터가 같은 방향이면 0, 직각이면 1, 정반대면 2 다(MariaDB 에서 실측한 값이 그대로 0·1·2 였다).

그래서 벡터 검색은 "단어가 들어 있나" 가 아니라 "뜻이 가까운가" 로 찾는다. 오타·동의어·다른 표현·심지어 다른 언어까지 한 번에 넘어간다. 대신 정확히 그 단어가 들어간 문서만 골라내는 일에는 약해서, 코드 식별자나 상품 코드 검색은 여전히 키워드 검색이 맞다. 둘을 같이 쓰는 경우가 많다.

어디에 쓰나

문서·FAQ 검색. 위의 예 그대로다. 규정집, 매뉴얼, 게시판 글을 임베딩해 두고 사용자의 질문 문장으로 찾는다. 검색어를 "잘" 골라야 하는 부담이 사라진다.

관련 글 추천. 지금 보는 글의 벡터에서 가까운 글 5개를 뽑으면 "이 글과 비슷한 글" 이 태그 없이 나온다. 태그를 붙이는 사람이 없어도 된다.

중복·유사 항목 감지. 새로 들어온 문의가 이미 답변된 문의와 거리가 0.1 이내면 "비슷한 문의가 있습니다" 를 띄운다. 게시판 중복 글, 상품 설명 중복도 같은 방식이다.

챗봇의 근거 찾기(RAG). 요즘 가장 큰 용도다. LLM 에 회사 규정을 물으면 모르거나 지어낸다. 그래서 답하기 전에 질문과 가까운 문서 조각을 벡터 검색으로 몇 개 찾아 프롬프트에 붙여 준다. "근거 문서 검색" 부분이 벡터 검색이고, 그 저장소가 벡터 DB 다.

이미지·음성 유사 검색. 이미지도 임베딩 모델을 거치면 벡터가 되므로 "이 사진과 비슷한 상품" 도 같은 쿼리로 된다.

공통점은 "정확히 일치" 가 아니라 "가장 가까운 N개" 를 원하는 자리라는 것이다. 이 모양이 보이면 벡터 검색을 떠올리면 된다.

MariaDB 11.7+ 에 들어온 것

벡터를 저장하고 거리를 재고 빠르게 찾으려면 세 가지가 필요한데, MariaDB 11.7 부터 셋 다 내장이다.

CREATE TABLE docs (
  id INT PRIMARY KEY AUTO_INCREMENT,
  title VARCHAR(200),
  embedding VECTOR(3) NOT NULL,              -- ① 타입 (괄호 안은 차원 수)
  VECTOR INDEX (embedding) DISTANCE=cosine   -- ③ 인덱스 (HNSW)
);

INSERT INTO docs (title, embedding)
VALUES ('휴가 결재 절차', VEC_FromText('[0.85,0.15,0.05]'));

SELECT title, VEC_DISTANCE_COSINE(embedding, VEC_FromText('[0.8,0.2,0]')) AS d  -- ② 거리 함수
FROM docs ORDER BY d LIMIT 3;

실측은 설명을 위해 3차원으로 했다(실제 임베딩은 1536차원 같은 크기이고 문법은 같다). 다섯 건을 넣고 "연차" 쪽 벡터로 물었더니 이렇게 나왔다.

title 코사인 거리 유클리드 거리
휴가 결재 절차0.00410.0866
연차 신청 방법0.00900.1414
출장비 청구0.60310.9407

"연차" 와 "휴가" 가 나란히 위에 오고 "출장비" 는 멀다. 거리 함수는 VEC_DISTANCE_COSINEVEC_DISTANCE_EUCLIDEAN 두 개이고, 인덱스를 만들 때 고른 거리와 쿼리의 거리 함수가 같아야 인덱스를 탄다. EXPLAIN 으로 보면 ORDER BY 거리 LIMIT n 형태에서 key: embedding 으로 인덱스를 썼다. WHERE 거리 <= 0.4 를 붙여도 마찬가지였다.

차원이 안 맞는 벡터는 넣을 때 거부된다. VECTOR(3) 칼럼에 [1,2] 를 넣으니 Incorrect vector value 오류가 났다. 임베딩 모델을 바꾸면 차원이 달라지므로 칼럼을 다시 만들고 전부 다시 임베딩해야 한다는 뜻이다.

Laravel 13 — 마이그레이션 두 줄, 쿼리 한 줄

Schema::create('docs', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->vector('embedding', dimensions: 1536);
    $table->vectorIndex('embedding');
});

MariaDB 연결에서 실제로 만들어진 테이블은 이렇다(SHOW CREATE TABLE).

`embedding` vector(1536) NOT NULL,
VECTOR KEY `docs_embedding_vectorindex` (`embedding`) `M`='6' `DISTANCE`='cosine'

인덱스 거리는 cosine 으로 고정되어 만들어지고, 쿼리 빌더도 코사인만 쓴다. 유클리드가 필요하면 아직 raw 로 가야 한다.

검색은 이렇게 쓴다.

$q = $embedder->embed('연차 어떻게 내요?');   // float 배열, 1536개

$docs = Doc::query()
    ->whereVectorSimilarTo('embedding', $q, minSimilarity: 0.4)
    ->limit(3)
    ->get();

MariaDB 연결에서 이 호출이 만든 SQL 과 바인딩을 toSql() 로 뽑았다.

select * from `docs`
where vec_distance_cosine(`embedding`, vec_fromtext(?)) <= ?
order by vec_distance_cosine(`embedding`, vec_fromtext(?)) asc
limit 3

-- bindings: ["[0.8,0.2,0]", 0.6, "[0.8,0.2,0]"]

세 가지가 보인다. 벡터 배열은 json_encode 되어 문자열로 바인딩되고 SQL 쪽에서 vec_fromtext() 가 받는다. minSimilarity: 0.41 - 0.4 = 0.6 으로 바뀌어 거리 ≤ 0.6 조건이 된다(유사도와 거리는 방향이 반대다). 그리고 기본으로 거리 오름차순 정렬이 붙는다. 정렬을 직접 하고 싶으면 order: false 를 넘긴다.

더 잘게 쓰고 싶으면 세 메서드를 따로 쓴다. 거리 값을 결과에 같이 받아 화면에 "유사도 96%" 같은 표시를 하려면 이쪽이다.

Doc::query()
    ->select('title')
    ->selectVectorDistance('embedding', $q, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $q, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $q)
    ->get();

// [{title: "휴가 결재 절차", distance: 0.00414}, {title: "연차 신청 방법", distance: 0.00901}]

실측에서 걸린 것

문자열을 그냥 넘기면 AI SDK 가 필요하다. 문서에는 whereVectorSimilarTo('embedding', '연차 어떻게 내요?') 처럼 문장을 바로 넘기면 임베딩을 자동 생성한다고 되어 있다. illuminate/database 만 설치한 상태에서 해 보니 Method Stringable::toEmbeddings does not exist 예외였다. 이 편의 기능은 Laravel AI SDK 가 설치되어 임베딩 모델이 설정돼 있어야 돈다. 없으면 임베딩은 직접 만들어 float 배열로 넘겨야 한다. 임베딩 호출은 외부 API 요금이 붙는 자리라, 자동으로 되는 게 편하기만 한 건 아니다 — 같은 질문을 캐시하는지, 어느 모델을 쓰는지는 알고 있어야 한다.

MariaDB 11.4 LTS 로는 안 된다. 흔히 깔려 있는 11.4 에는 VECTOR 타입이 없다. 11.7 부터이고 LTS 는 11.8 이다. 실측도 mariadb:11.8 도커 이미지로 했다. MySQL 은 이 기능이 없어서 Laravel 의 지원 목록도 PostgreSQL(pgvector)과 MariaDB 둘뿐이다.

인덱스 파라미터를 못 건드린다. vectorIndex()M=6·DISTANCE=cosine 기본값으로만 만든다. HNSW 의 M 은 정확도와 메모리를 맞바꾸는 값이라 수십만 건 이상이면 조정하고 싶어지는데, 그때는 DB::statement() 로 직접 만든다.

임베딩은 저장할 때 만든다. 검색할 때는 질문 하나만 임베딩하면 되지만, 문서 쪽은 저장·수정될 때마다 임베딩을 다시 만들어 칼럼에 넣어 두어야 한다. 모델 옵저버나 큐 잡으로 처리하는 것이 보통이다. 이 부분은 Laravel Scout 가 관장하는 영역이라 Scout 도 같은 시기에 의미 검색 쪽으로 확장됐다.

정리

벡터 검색은 문장을 임베딩 모델로 숫자 배열(벡터)로 바꿔 저장해 두고, 질문 벡터에서 가장 가까운 N개를 찾는 방식이다. 글자가 아니라 뜻으로 찾으므로 "연차" 로 "휴가" 문서가 나온다. 문서 검색, 관련 글 추천, 중복 감지, 챗봇의 근거 찾기(RAG)가 주 용도다.

MariaDB 11.7+ 에 VECTOR 타입·VEC_DISTANCE_COSINE·VECTOR INDEX 가 내장되어 별도 벡터 DB 없이 기존 테이블 옆에 칼럼 하나로 시작할 수 있고, Laravel 13 은 $table->vector()whereVectorSimilarTo() 로 이를 감싼다. minSimilarity1 - x 로 거리 조건이 되고, 배열은 JSON 문자열로 바인딩되어 vec_fromtext() 로 들어간다. 문장을 직접 넘기는 편의 기능은 AI SDK 가 있어야 하고, 서버 MariaDB 는 11.8 이어야 한다.