전문 검색
개요
AgensGraph는 PostgreSQL을 기반으로 구축되어 그 전문 검색 엔진을 그대로 물려받으므로, 그래프 속성에 저장된 텍스트에 대해 언어적 키워드 검색을 직접 실행할 수 있습니다. 전문 검색은 문서와 쿼리를 렉심(lexeme)으로 정규화하며(어간 추출, 불용어 제거, 대소문자 변환), 이로 인해 사용자가 실제로 입력한 단어를 매칭하는 어휘적(lexical) 검색에 적합합니다. 이는 의미 기반으로 매칭하는 벡터 검색을 자연스럽게 보완하며, 두 방식을 결합하는 방법은 하이브리드 검색에서 다룹니다. 세 페이지 모두 동일한 영화 데이터를 사용하므로 결과를 직접 비교할 수 있습니다.
그래프 속성은 jsonb로 저장되므로, 텍스트 속성은 tsvector로 변환되기 전에 text로 캐스팅됩니다(::text 사용).
예제 데이터
예제는 영화 목록을 사용하며, 각 영화에는 plot 텍스트 속성이 있습니다(벡터 검색과 동일한 영화로, 해당 페이지에서는 plot을 임베딩하기도 합니다):
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]});
텍스트 속성 검색
to_tsvector로 속성을 tsvector로 변환하고, *_to_tsquery 함수 중 하나로 사용자 입력을 파싱한 다음, @@ 연산자로 둘을 매칭합니다. websearch_to_tsquery는 관대한 웹 스타일 구문(따옴표로 묶은 구절, OR, 그리고 특정 용어를 제외하는 -)을 허용하므로, 사용자 입력에 대한 기본값으로 적합합니다.
MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'machines')
RETURN m.title AS title;
title
----------------------------
"The Matrix"
"The Matrix Reloaded"
"The Matrix Revolutions"
"The Matrix Resurrections"
(4 rows)
쿼리는 원시 텍스트가 아니라 렉심에 대해 매칭됩니다. machines는 어간 machin으로 축소되므로, 단수형 machine 역시 정확히 동일한 행과 매칭됩니다. 또한 반환되지 않은 것에도 주목하십시오. Total Recall과 The Terminator는 주제 면에서 기계가 지배하는 미래를 다루지만, 그 plot에서는 해당 단어를 전혀 사용하지 않으므로 키워드 검색으로는 찾을 수 없습니다. 바로 이 재현율(recall) 격차가 벡터 검색이 도움을 주는 지점입니다.
기대하는 입력에 따라 다른 쿼리 파서를 사용할 수 있습니다:
함수 |
입력 스타일 |
|---|---|
|
웹 스타일: |
|
일반 텍스트, 모든 용어를 |
|
명시적 연산자: |
결과 순위 매기기
ts_rank는 각 문서가 쿼리와 얼마나 잘 매칭되는지를 점수로 매기므로, 결과를 관련성 순으로 정렬할 수 있습니다. 쿼리 용어를 더 많이 매칭하는 plot일수록 더 높은 순위를 차지합니다:
MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'machines OR reality')
RETURN m.title AS title,
ts_rank(to_tsvector('english', m.plot::text),
websearch_to_tsquery('english', 'machines OR reality')) AS rank
ORDER BY rank DESC;
title | rank
----------------------------+-------------
"The Matrix" | 0.06079271
"The Matrix Reloaded" | 0.030396355
"The Matrix Revolutions" | 0.030396355
"The Matrix Resurrections" | 0.030396355
(4 rows)
The Matrix가 속편들보다 높은 순위를 차지하는 이유는, 그 plot이 machines와 reality를 모두 포함하는 반면 속편들은 machines만 매칭하기 때문입니다.
정확한 단어가 중요한 경우
전문 검색은 쿼리가 정확히 매칭되어야 하는 특정 용어, 즉 이름, 코드, 드문 키워드일 때 적합한 도구입니다. dinosaurs에 관한 영화를 요청하면 정확히 하나를 반환합니다:
MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'dinosaurs')
RETURN m.title AS title;
title
-----------------
"Jurassic Park"
(1 row)
벡터 검색에는 dinosaurs라는 문자 그대로의 단어 개념이 없습니다. Jurassic Park과 가장 유사한 영화를 요청하면 최근접 이웃으로 The Avengers를 반환합니다. 둘 다 대규모 스펙터클 블록버스터로, 의미 면에서는 합리적인 추측이지만 “dinosaurs”를 입력하는 사람이 원하는 것은 아닙니다:
MATCH (m:movie), (q:movie {title: 'Jurassic Park'})
RETURN m.title AS title
ORDER BY m.embedding::vector(4) <=> q.embedding::vector(4)
LIMIT 3;
title
-----------------
"Jurassic Park"
"The Avengers"
"Interstellar"
(3 rows)
따라서 두 방식은 정반대의 맹점을 가집니다. 전문 검색은 정확하지만 동의어와 의도를 놓치는 반면, 벡터 검색은 의미를 포착하지만 정확한 용어에서는 벗어납니다. 하이브리드 검색은 두 방식을 모두 실행하고 결과를 융합하여 각각의 강점을 얻습니다.
GIN을 사용한 인덱싱
검색할 때마다 모든 행에 대해 to_tsvector를 재계산하는 것은 확장성이 없습니다. 동일한 to_tsvector 표현식에 GIN 인덱스를 생성하여 인덱스를 통해 매칭이 이루어지도록 하십시오. 인덱싱되는 값이 속성에 대한 표현식이므로, property index를 사용하고 표현식을 괄호로 묶으십시오:
CREATE PROPERTY INDEX ON movie
USING gin ((to_tsvector('english', plot::text)));
그러면 검색은 모든 행을 스캔하는 대신 인덱스를 사용합니다:
EXPLAIN
MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'machines')
RETURN m.title;
Bitmap Heap Scan on movie m
Recheck Cond: (to_tsvector('english'::regconfig, (properties.'plot'::text)::text) @@ '''machin'''::tsquery)
-> Bitmap Index Scan on movie_plot_idx
Index Cond: (to_tsvector('english'::regconfig, (properties.'plot'::text)::text) @@ '''machin'''::tsquery)
인덱스가 사용되려면 쿼리 시점에 사용되는 텍스트 검색 설정(위의 'english')이 인덱스 표현식의 설정과 일치해야 합니다.
이 작은 데모에서는 행 수가 매우 적어 순차 스캔이 더 저렴하기 때문에 플래너가 여전히 순차 스캔을 선호할 수 있습니다. 인덱스 계획을 관찰하려면
SET enable_seqscan = off를 추가하십시오. 실제 코퍼스(corpus)에서는 플래너가 자동으로 인덱스를 선택합니다.
to_tsvector(...)표현식에 대한 GIN property index는 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 쿼리로 질문(여기서는 resistance라는 용어)에 대한 전문 검색 관련성에 따라 영화의 순위를 매기고, 가장 잘 매칭되는 항목을 유지한 다음, 이를 인접 노드로 확장합니다:
MATCH (seed:movie)
WHERE to_tsvector('english', seed.plot::text) @@ websearch_to_tsquery('english', 'resistance')
WITH seed, ts_rank(to_tsvector('english', seed.plot::text),
websearch_to_tsquery('english', 'resistance')) AS rank
ORDER BY rank DESC, seed.title
LIMIT 2
MATCH (seed)-[:related_to]-(ctx:movie)
RETURN seed.title AS seed, collect(DISTINCT ctx.title) AS related_context;
seed | related_context
------------------------------+----------------------------------------------
"Terminator 2: Judgment Day" | ["The Terminator"]
"The Terminator" | ["Terminator 2: Judgment Day", "The Matrix"]
(2 rows)
WHERE 필터와 WITH … ORDER BY rank LIMIT는 텍스트가 쿼리와 가장 잘 매칭되는 영화(시드(seed) — 두 편의 Terminator 영화)를 선택합니다. 두 번째 MATCH는 related_to 간선을 따라가며 관련 작품을 수집합니다. 시드와 그 인접 노드가 함께 모델에 전달되는 컨텍스트를 형성하며, 순회가 The Matrix에 도달한다는 점에 주목하십시오. The Matrix는 키워드 resistance로는 전혀 매칭되지 않았지만, 기계와의 전쟁이라는 공통 주제로 Terminator 영화들과 연결되어 있습니다. 이 어휘적 검색을 확장 이전에 의미적(벡터) 검색과 결합하려면 하이브리드 검색을 참조하십시오.