벡터 검색

개요

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를 추가하세요. 실제 코퍼스에서는 플래너가 자동으로 인덱스를 선택합니다.

::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 파이프라인에 필요한 정확한 컨텍스트를 구성할 수 있습니다.