{#full-text-full-text-search} # 전문 검색 {#full-text-overview} ## 개요 AgensGraph는 PostgreSQL 위에 만들어졌고 그 전문 검색 엔진을 그대로 물려받으므로, 그래프 속성에 저장한 텍스트를 상대로 언어적 키워드 검색을 곧바로 할 수 있습니다. 전문 검색은 문서와 쿼리를 어휘소로 정규화합니다(어간 추출·불용어 제거·대소문자 접기). 그래서 사용자가 입력한 단어를 대조하는 *어휘* 검색에 잘 맞습니다. *의미*로 대조하는 [벡터 검색](vector.md)과 자연스럽게 짝을 이룹니다. 둘을 결합하는 것은 [하이브리드 검색](hybrid.md)이 다룹니다. 세 페이지가 같은 영화를 쓰므로 결과를 직접 견주어 볼 수 있습니다. 그래프 속성은 `jsonb`로 저장되므로, 텍스트 속성은 `tsvector`로 바꾸기 전에 (`::text`로) `text`로 형 변환합니다. {#full-text-example-data} ## 예제 데이터 예제는 [예제 데이터셋](../../get_started/example-dataset.md)을 씁니다. 영화 18편이 각각 `plot`과 그 줄거리의 4차원 `embedding`을 가지고 있습니다. 더 가기 전에 그것을 적재하십시오. 실제 임베딩은 `text-embedding-3-small`이나 로컬 `sentence-transformers` 모델 같은 모델에서 나오며 차원이 수백 개입니다. 여기서는 지면에서 기하 구조가 읽히도록 넷으로 줄였습니다. {#full-text-searching-a-text-property} ## 텍스트 속성 검색하기 `to_tsvector`로 속성을 `tsvector`로 바꾸고, `*_to_tsquery` 함수 중 하나로 사용자의 입력을 파싱한 뒤, `@@` 연산자로 대조하십시오. `websearch_to_tsquery`는 너그러운 웹 스타일 구문을 받습니다. 따옴표로 감싼 구절, `OR`, 그리고 단어를 빼는 `-`입니다. 그래서 사용자 입력에는 이것이 좋은 기본값입니다. ```cypher 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*는 주제로 보면 기계가 지배하는 미래를 다루지만 줄거리에 그 단어를 쓰지 않았으므로 키워드 검색이 찾지 못합니다. 그 재현율의 빈틈이 바로 [벡터 검색](vector.md)이 돕는 지점입니다. 기대하는 입력에 따라 다른 쿼리 파서도 쓸 수 있습니다: | 함수 | 입력 형태 | | ------------------------ | -------------------------------------------------- | | `websearch_to_tsquery` | 웹 스타일: `machines "virtual reality" -documentary` | | `plainto_tsquery` | 평문이며 모든 단어를 `AND`로 묶습니다 | | `to_tsquery` | 연산자를 직접 씁니다: `machines & reality` | {#full-text-ranking-results} ## 결과 순위 매기기 `ts_rank`는 각 문서가 쿼리에 얼마나 잘 맞는지 점수를 매기므로 결과를 관련도로 정렬할 수 있습니다. 쿼리 단어를 더 많이 맞힌 줄거리가 더 높은 순위를 받습니다: ```cypher 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*가 속편들보다 위에 오는 것은 그 줄거리가 `machines`와 `reality`를 **둘 다** 담고 있는 반면 속편들은 `machines`만 맞히기 때문입니다. {#full-text-exact-term-matching} ## 정확한 단어 대조 쿼리가 정확히 맞아야 하는 특정 단어 — 이름, 코드, 드문 키워드 — 일 때는 전문 검색이 알맞은 도구입니다. *dinosaurs*에 관한 영화를 물으면 정확히 하나를 돌려줍니다: ```cypher 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) ``` [벡터 검색](vector.md)에는 `dinosaurs`라는 글자 그대로의 단어라는 개념이 없습니다. *Jurassic Park*과 가장 비슷한 영화를 물으면 가장 가까운 이웃으로 *The Avengers*를 돌려줍니다. 둘 다 대규모 볼거리 블록버스터이니 *의미*로는 그럴듯한 짐작이지만, "dinosaurs"를 입력한 사람이 원하는 것은 아닙니다: ```cypher 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) ``` 그러므로 두 방법은 서로 반대쪽에 사각지대를 가집니다. 전문 검색은 정확하지만 동의어와 의도를 놓치고, 벡터 검색은 의미를 잡아내지만 정확한 단어에서 흔들립니다. [하이브리드 검색](hybrid.md)은 둘 다 실행하고 결과를 합쳐 각각의 강점을 취합니다. {#full-text-indexing-with-gin} ## GIN으로 인덱싱하기 검색할 때마다 모든 행에 대해 `to_tsvector`를 다시 계산하는 것은 규모를 감당하지 못합니다. 인덱스로 일치를 찾도록 같은 `to_tsvector` 표현식에 GIN 인덱스를 만드십시오. 인덱싱하는 값이 속성에 대한 표현식이므로 **속성 인덱스**를 쓰고 표현식을 괄호로 감싸십시오: ```sql CREATE PROPERTY INDEX ON movie USING gin ((to_tsvector('english', plot::text))); ``` 그러면 검색이 모든 행을 스캔하는 대신 인덱스를 씁니다: ```psql 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`를 더하십시오. 실제 말뭉치에서는 플래너가 알아서 인덱스를 고릅니다. > `to_tsvector(...)` 표현식에 대한 GIN 속성 인덱스는 AgensGraph 2.17 이상이 필요합니다. {#full-text-indexing-a-promoted-column} ## 승격된 컬럼 인덱싱하기 2.18부터는 속성을 [실제 타입 컬럼으로 승격](../../querying/typed-columns.md)할 수 있으며, 그러면 행마다의 `jsonb` 읽기와 `::text` 형 변환이 사라집니다. `ALTER VLABEL movie ADD COLUMN plot text GENERATED`는 컬럼 이름이 가리키는 속성, 여기서는 `plot`을 비춥니다. 이름이 다른 키를 비추려면 `GENERATED (sourceKey)`를 씁니다. 쿼리는 컬럼이 아니라 속성의 이름을 계속 댑니다 — 해석은 속성 키로 이루어집니다. 이 페이지의 인덱스는 속성에 대한 *표현식*이므로 승격할 때 다시 만들어야 합니다. 표현식에 대한 속성 인덱스는 jsonb `properties`를 상대로 만들어지고 승격된 키에 대한 쿼리는 컬럼을 읽으므로, 둘은 더는 만나지 않게 됩니다. 이것은 컬럼에 대한 SQL로 쓰십시오. `\dGi`는 여전히 그것을 나열하고, 쿼리도 그것을 씁니다: ```cypher DROP PROPERTY INDEX movie_plot_idx; ALTER VLABEL movie ADD COLUMN plot text GENERATED; ``` ```sql CREATE INDEX movie_plot_idx ON moviekb.movie USING gin (to_tsvector('english', plot)); ``` ```psql EXPLAIN (COSTS OFF) MATCH (m:movie) WHERE to_tsvector('english', m.plot) @@ websearch_to_tsquery('english', 'machines') RETURN m.title; ``` ``` Bitmap Heap Scan on movie m Recheck Cond: (to_tsvector('english'::regconfig, plot) @@ '''machin'''::tsquery) -> Bitmap Index Scan on movie_plot_idx Index Cond: (to_tsvector('english'::regconfig, plot) @@ '''machin'''::tsquery) ``` 이제 `m.plot`에는 `::text`가 필요 없습니다. 승격된 컬럼이 이미 `text`이기 때문입니다. [표현식 속성 인덱스는 `properties`에 걸린다](#typed-columns-an-expression-property-index-is-built-over-the-bag)를 참고하십시오. {#full-text-graphrag} ## GraphRAG 전문 검색도 그래프 순회의 시드가 될 수 있습니다. 질문에 텍스트가 가장 잘 맞는 영화를 찾은 다음, 관계를 따라 뻗어 나가 언어 모델에 줄 연결된 맥락을 모읍니다. [벡터 검색](vector.md)의 GraphRAG 예제에 대응하는 어휘 쪽 예제입니다. 임베딩보다 키워드 검색이 잘 다루는 특정 단어(이름·코드·정확한 구절)이 질문에 들어 있을 때 빛을 발합니다. [예제 데이터셋](../../get_started/example-dataset.md)에는 영화 사이의 `related_to` 간선이 이미 들어 있습니다. 시리즈 속편과 주제로 이어진 작품들입니다. 그래서 아래 순회가 따라갈 것이 있습니다. Cypher 쿼리 하나가 질문(여기서는 단어 `resistance`)에 대한 전문 검색 관련도로 영화의 순위를 매기고, 가장 잘 맞는 것을 남긴 뒤 그 이웃으로 뻗어 나갑니다: ```cypher 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`가 쿼리에 텍스트가 가장 잘 맞는 영화를 고릅니다. 그것이 *시드*이며, 여기서는 *Terminator* 두 편입니다. 두 번째 `MATCH`는 `related_to` 간선을 따라가 관련 작품을 모읍니다. 시드와 그 이웃이 모델에 넘길 맥락을 이룹니다. 순회가 *The Matrix*까지 닿는 것에 유의하십시오. 키워드 `resistance`는 그 영화와 일치한 적이 없지만, 기계와의 전쟁이라는 공통 주제로 *Terminator* 영화들과 이어져 있습니다. 뻗어 나가기 전에 이 어휘 검색을 의미(벡터) 검색과 결합하려면 [하이브리드 검색](hybrid.md)을 참고하십시오. 같은 검색을 손으로 짜는 대신 프레임워크로 하려면 [AI·LLM 연동](../../drivers/ai-integrations.md)을 참고하십시오.