벡터 검색

개요

AgensGraph는 PostgreSQL을 기반으로 구축되므로 pgvector 익스텐션을 직접 사용하여 그래프에 벡터 유사도 검색을 추가할 수 있습니다. 이를 통해 임베딩을 그래프 데이터와 함께 보관하고, 단일 데이터베이스 안에서 시맨틱(벡터) 검색과 Cypher 그래프 순회를 결합할 수 있습니다. 이는 검색 증강 생성(RAG)과 GraphRAG 애플리케이션의 토대가 됩니다.

벡터 검색은 단어가 아니라 의미로 매칭합니다. 두 텍스트가 어휘를 전혀 공유하지 않더라도 벡터 공간에서는 가까울 수 있습니다. 이것이 키워드 매칭(전문 검색) 대비 벡터 검색이 가지는 강점이며, 두 방식을 결합하는 방법은 하이브리드 검색에서 다룹니다. 세 페이지의 예제는 모두 동일한 영화 집합을 사용하므로 동작을 직접 비교할 수 있습니다.

AgensGraph는 네이티브 벡터 타입을 제공하지 않습니다. 대신 임베딩은 정점(또는 간선)의 숫자 배열 속성으로 저장되며, 벡터 연산이 필요할 때마다 ::vector(n)을 사용하여 pgvector의 vector 타입으로 캐스팅됩니다. 속성은 jsonb로 저장되므로 배열은 JSON 배열로 보관되었다가 필요 시점에 변환됩니다.

익스텐션 설치

pgvector는 별도의 익스텐션이며, 사용 중인 AgensGraph 설치본에 맞춰 빌드하고 설치해야 합니다. 사용 가능해지면 데이터베이스별로 활성화합니다:

CREATE EXTENSION vector;

예제 데이터

예제에서는 작은 영화 목록을 사용합니다. 각 영화에는 plot과 해당 줄거리의 embedding이 있는데, 이 임베딩은 의미적으로 유사한 줄거리를 서로 가깝게 배치하는 벡터입니다. 실제 임베딩은 모델(예: OpenAI text-embedding-3-small 또는 로컬 sentence-transformers 모델)에서 생성되며 보통 수백 개의 차원을 가집니다. 아래 벡터는 기하 구조를 읽기 쉽게 유지하기 위해 4개 차원으로 축소되어 있습니다.

CREATE GRAPH moviekb;
SET graph_path = moviekb;
CREATE VLABEL movie;

CREATE (:movie {title: 'The Matrix', year: 1999, genre: 'Action', plot: 'A computer hacker learns about the true nature of reality and joins a rebellion to free humanity from a simulated world controlled by machines.', embedding: [-0.07594558, 0.04081754, 0.29592122, -0.11921061]}),
       (:movie {title: 'The Matrix Reloaded', year: 2003, genre: 'Action', plot: 'The rebels continue their fight against the machines, uncovering deeper truths about the Matrix and the nature of their mission.', embedding: [0.30228977, -0.22839354, 0.35070436, 0.01262819]}),
       (:movie {title: 'The Matrix Revolutions', year: 2003, genre: 'Action', plot: 'The final battle between humans and machines reaches its climax as the fate of both worlds hangs in the balance.', embedding: [0.12240622, -0.29752459, 0.22620453, 0.24454723]}),
       (:movie {title: 'The Matrix Resurrections', year: 2021, genre: 'Action', plot: 'Neo returns to a new version of the Matrix and must once again fight to save the people from the control of the machines.', embedding: [0.34717246, -0.13820869, 0.29214213, 0.08090488]}),
       (:movie {title: 'Inception', year: 2010, genre: 'Sci-Fi', plot: 'A skilled thief is given a chance at redemption if he can successfully perform an inception: planting an idea into someone''s subconscious.', embedding: [0.03923657, 0.39284106, -0.20927092, -0.17770818]}),
       (:movie {title: 'Interstellar', year: 2014, genre: 'Sci-Fi', plot: 'A group of explorers travel through a wormhole in space in an attempt to ensure humanity''s survival.', embedding: [-0.29302418, -0.39615033, -0.23393948, -0.09601383]}),
       (:movie {title: 'Avatar', year: 2009, genre: 'Sci-Fi', plot: 'A paraplegic Marine is sent to the moon Pandora, where he becomes torn between following orders and protecting the world he feels is his home.', embedding: [-0.13663386, 0.00635589, -0.03038832, -0.08252723]}),
       (:movie {title: 'Blade Runner', year: 1982, genre: 'Sci-Fi', plot: 'A blade runner must pursue and terminate four replicants who have stolen a ship in space and returned to Earth.', embedding: [0.27215557, -0.1479577, -0.09972772, -0.08234394]}),
       (:movie {title: 'Blade Runner 2049', year: 2017, genre: 'Sci-Fi', plot: 'A new blade runner unearths a long-buried secret that has the potential to plunge what''s left of society into chaos.', embedding: [0.21560573, -0.07505179, -0.01331814, 0.13403069]}),
       (:movie {title: 'Minority Report', year: 2002, genre: 'Sci-Fi', plot: 'In a future where a special police unit can arrest murderers before they commit their crimes, a top officer is accused of a future murder.', embedding: [0.24008012, 0.44954908, -0.30905488, 0.15195407]}),
       (:movie {title: 'Total Recall', year: 1990, genre: 'Sci-Fi', plot: 'A construction worker discovers that his memories have been implanted and becomes embroiled in a conspiracy on Mars.', embedding: [-0.17471036, 0.14695261, -0.06272433, -0.21795064]}),
       (:movie {title: 'Elysium', year: 2013, genre: 'Sci-Fi', plot: 'In a future where the rich live on a luxurious space station while the rest of humanity lives in squalor, a man fights to bring equality.', embedding: [-0.33280967, 0.07733926, 0.11015328, 0.53382836]}),
       (:movie {title: 'Gattaca', year: 1997, genre: 'Sci-Fi', plot: 'In a future where genetic engineering determines social class, a man defies his fate to achieve his dreams.', embedding: [-0.21629286, 0.31114665, 0.08303899, 0.46199759]}),
       (:movie {title: 'The Fifth Element', year: 1997, genre: 'Sci-Fi', plot: 'In a futuristic world, a cab driver becomes the key to saving humanity from an impending cosmic threat.', embedding: [-0.11528205, -0.0208782, -0.0735215, 0.14327449]}),
       (:movie {title: 'The Terminator', year: 1984, genre: 'Action', plot: 'A cyborg assassin is sent back in time to kill the mother of the future resistance leader.', embedding: [0.33666933, 0.18040994, -0.01075103, -0.11117851]}),
       (:movie {title: 'Terminator 2: Judgment Day', year: 1991, genre: 'Action', plot: 'A reprogrammed Terminator is sent to protect the future leader of the human resistance from a more advanced Terminator.', embedding: [0.34698868, 0.06439331, 0.06232323, -0.19534876]}),
       (:movie {title: 'Jurassic Park', year: 1993, genre: 'Adventure', plot: 'Scientists clone dinosaurs to create a theme park, but things go awry when the creatures escape.', embedding: [0.01794725, -0.11434246, -0.46831815, -0.01049593]}),
       (:movie {title: 'The Avengers', year: 2012, genre: 'Action', plot: 'Superheroes assemble to face a global threat from an alien invasion led by Loki.', embedding: [0.00546514, -0.37005171, -0.42612838, 0.07968612]});

저장된 임베딩을 벡터로 사용하려면 속성을 캐스팅합니다:

MATCH (m:movie)
RETURN m.title, m.embedding::vector(4);

유사도 검색

pgvector는 거리 연산자와 매칭 함수 집합을 제공합니다. 두 피연산자를 모두 vector(n)으로 캐스팅하고 거리 기준으로 정렬합니다.

연산자

함수

거리

HNSW 연산자 클래스

<=>

cosine_distance

코사인 거리

vector_cosine_ops

<->

l2_distance

유클리드(L2)

vector_l2_ops

<#>

inner_product

(음의) 내적

vector_ip_ops

<+>

l1_distance

맨해튼 거리(L1)

vector_l1_ops

쿼리 벡터에 줄거리가 가장 가까운 영화를 찾습니다(코사인 거리). 여기서 쿼리는 The Matrix의 줄거리 임베딩이므로 검색은 의미적으로 가장 유사한 영화들을 반환합니다:

MATCH (m:movie)
RETURN m.title AS title
ORDER BY m.embedding::vector(4) <=> '[-0.07594558, 0.04081754, 0.29592122, -0.11921061]'::vector(4)
LIMIT 4;
           title
----------------------------
 "The Matrix"
 "The Matrix Reloaded"
 "The Matrix Resurrections"
 "Total Recall"
(4 rows)

처음 세 결과는 당연한 속편이지만, 네 번째 결과가 흥미로운 부분입니다. Total Recall의 줄거리(“건설 노동자가 자신의 기억이 이식되었음을 알게 된다”)는 The Matrix의 *”해커가 … 기계가 통제하는 시뮬레이션 세계”*와 공통 키워드가 전혀 없습니다. 그럼에도 벡터 검색이 이를 네 번째로 순위 매긴 이유는 두 영화 모두 주인공이 자신의 현실이 조작된 것임을 깨닫는다는 내용이기 때문입니다. 키워드 검색으로는 이 둘을 결코 연결할 수 없습니다. 이것이 바로 시맨틱 검색은 포착하지만 어휘 기반 검색은 포착하지 못하는 부분입니다. 동일한 데이터에 대한 전문 검색 결과와 비교해 보고, 두 방식을 결합하는 방법은 하이브리드 검색을 참고하십시오.

ORDER BY는 거리 표현식이 RETURN 목록에 없더라도 이를 참조할 수 있습니다(AgensGraph 2.17 이상). title만 반환하면 출력이 깔끔하게 유지되며, 더 중요하게는 HNSW 인덱스가 정렬을 처리할 수 있습니다. 원시 거리 값을 반환하면 그 값이 jsonb로 변환되어 인덱스가 사용되지 못하게 됩니다.

함수 형태는 쿼리 벡터를 직접 작성하지 않고 다른 저장된 벡터와 비교할 때, 예를 들어 “The Terminator와 가장 유사한 영화”를 찾을 때 편리합니다:

MATCH (m:movie), (q:movie {title: 'The Terminator'})
RETURN m.title AS title
ORDER BY m.embedding::vector(4) <=> q.embedding::vector(4)
LIMIT 4;
            title
----------------------------
 "The Terminator"
 "Terminator 2: Judgment Day"
 "Minority Report"
 "Blade Runner"
(4 rows)

HNSW를 사용한 인덱싱

인덱스가 없으면 모든 유사도 쿼리는 전체 행을 스캔하여 각 행에 대한 거리를 계산합니다. 더 큰 데이터셋의 경우, 플래너가 인덱스 스캔을 사용할 수 있도록 HNSW 인덱스를 생성하십시오. 인덱싱되는 값이 캐스팅 표현식이므로 속성 인덱스를 사용합니다:

CREATE PROPERTY INDEX ON movie
    USING hnsw ((embedding::vector(4)) vector_cosine_ops);

연산자 클래스는 쿼리에 사용하는 거리에 맞춰야 합니다(<=>에는 vector_cosine_ops, <->에는 vector_l2_ops 등). 그러면 앞의 유사도 쿼리가 인덱스를 사용하게 됩니다:

EXPLAIN
MATCH (m:movie)
RETURN m.title AS title
ORDER BY m.embedding::vector(4) <=> '[-0.07594558, 0.04081754, 0.29592122, -0.11921061]'::vector(4)
LIMIT 4;
 Limit
   ->  Index Scan using movie_embedding_idx on movie m
         Order By: ((properties.'embedding'::text)::vector(4) <=> '[...]'::vector(4))

행이 18개뿐인 이 데모에서는 행 수가 매우 적어 순차 스캔이 더 저렴하므로 플래너가 여전히 순차 스캔을 선호할 수 있습니다. 인덱스 계획을 확인하려면 SET enable_seqscan = off를 추가하십시오. 실제 코퍼스(corpus)에서는 플래너가 인덱스를 자동으로 선택합니다.

::vector(n) 캐스팅에 대한 HNSW 속성 인덱스에는 AgensGraph 2.17 이상이 필요합니다.

GraphRAG: 벡터 검색과 그래프 순회의 결합

그래프 데이터베이스 내부에서 벡터 검색을 실행하는 것의 강점은 시맨틱 매칭이 그래프 순회의 시작점이 될 수 있다는 점입니다. 일반적인 GraphRAG 검색은 하나의 쿼리에서 두 가지를 수행합니다. 먼저 벡터 유사도로 질문과 가장 관련성이 높은 항목을 찾은 다음, 관계를 따라 확장하여 언어 모델을 위한 연결된 컨텍스트를 수집합니다.

영화 그래프에 몇 개의 related_to 간선, 즉 프랜차이즈 속편과 주제적으로 연결된 영화를 추가하여 보강합니다:

CREATE ELABEL related_to;

MATCH (a:movie {title: 'The Matrix'}),             (b:movie {title: 'The Matrix Reloaded'})       CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'The Matrix Reloaded'}),    (b:movie {title: 'The Matrix Revolutions'})     CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'The Matrix Revolutions'}), (b:movie {title: 'The Matrix Resurrections'})   CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'The Matrix'}),             (b:movie {title: 'Total Recall'})               CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'The Matrix'}),             (b:movie {title: 'The Terminator'})             CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'Total Recall'}),           (b:movie {title: 'Inception'})                  CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'The Terminator'}),         (b:movie {title: 'Terminator 2: Judgment Day'}) CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'Blade Runner'}),           (b:movie {title: 'Blade Runner 2049'})          CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'Interstellar'}),           (b:movie {title: 'Avatar'})                     CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'Jurassic Park'}),          (b:movie {title: 'The Avengers'})               CREATE (a)-[:related_to]->(b);
MATCH (a:movie {title: 'Minority Report'}),        (b:movie {title: 'Inception'})                  CREATE (a)-[:related_to]->(b);

쿼리 시점에는 줄거리에 사용한 것과 동일한 모델로 사용자의 질문을 임베딩한 다음, 가장 가까운 영화를 검색하고 그 이웃으로 확장하는 단일 Cypher 쿼리를 실행합니다. 여기서 질문 임베딩은 The Matrix의 벡터입니다:

MATCH (seed:movie)
WITH seed
ORDER BY seed.embedding::vector(4) <=> '[-0.07594558, 0.04081754, 0.29592122, -0.11921061]'::vector(4)
LIMIT 2
MATCH (seed)-[:related_to]-(ctx:movie)
RETURN seed.title AS seed, collect(DISTINCT ctx.title) AS related_context;
         seed          |                      related_context
-----------------------+-----------------------------------------------------------
 "The Matrix"          | ["The Matrix Reloaded", "The Terminator", "Total Recall"]
 "The Matrix Reloaded" | ["The Matrix", "The Matrix Revolutions"]
(2 rows)

WITH … ORDER BY … LIMIT 2는 벡터 유사도로 질문과 가장 잘 매칭되는 두 영화(시드)를 선택합니다. 두 번째 MATCH는 각 시드에서 related_to 간선을 따라가며 이웃 영화를 수집합니다. 시드와 그 그래프 이웃을 합친 집합은 벡터 검색만 사용할 때보다 풍부한 컨텍스트를 형성하며, 언어 모델에 전달하여 최종 답변을 생성할 수 있습니다.

이 패턴은 일반화할 수 있습니다. 시드는 여러 홉에 걸쳐 확장하거나, 간선 타입 또는 속성으로 필터링하거나, 다른 레이블(인물, 스튜디오, 출처)에 조인하여 RAG 파이프라인에 필요한 컨텍스트를 정확하게 구성할 수 있습니다.