검색 색인

이 페이지에서는 검색 색인을 추가하고 사용하는 방법을 설명합니다. 전체 텍스트 검색은 검색 색인의 항목을 대상으로 실행됩니다.

검색 색인 사용 방법

전체 텍스트 검색에 사용할 열에 검색 색인을 만들 수 있습니다. 검색 색인을 만들려면 CREATE SEARCH INDEX DDL 문을 사용합니다. 색인을 업데이트하려면 ALTER SEARCH INDEX DDL 문을 사용합니다. Spanner는 데이터베이스에서 데이터가 변경되는 즉시 검색 색인에서 데이터를 추가 및 업데이트하는 등 검색 색인을 자동으로 빌드하고 유지보수합니다.

검색 색인 파티션

검색 색인은 가속하려는 쿼리 유형에 따라 파티션을 나눈 또는 파티션을 나누지 않은 색인일 수 있습니다.

  • 파티션을 나눈 색인이 가장 적합한 예는 애플리케이션이 이메일 편지함을 쿼리하는 경우입니다. 각 쿼리가 특정 편지함으로 제한됩니다.

  • 파티션을 나누지 않은 쿼리가 가장 적합한 예는 제품 카탈로그의 모든 제품 카테고리에 대한 쿼리가 있는 경우입니다.

검색 색인 사용 사례

Spanner 검색 색인은 전체 텍스트 검색 외에도 다음을 지원합니다.

  • JSON 검색: JSON 및 JSONB 문서를 색인화하고 쿼리하는 효율적인 방법입니다.
  • 하위 문자열 검색: 더 긴 텍스트 본문 내에서 더 짧은 문자열(하위 문자열)을 찾는 쿼리 유형입니다.
  • 일치검색 및 숫자를 비롯하여 색인이 생성된 데이터의 하위 집합에 대한 조건을 단일 색인 스캔으로 결합

사용 사례에 관한 자세한 내용은 검색과 보조 색인 비교를 참조하세요.

검색 색인 예시

검색 색인의 기능을 설명하기 위해 음악 앨범에 대한 정보를 저장하는 테이블이 있다고 가정해 보겠습니다.

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX)
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
PRIMARY KEY(albumid));

Spanner에는 토큰을 만드는 여러 토큰화 함수가 있습니다. 사용자가 전체 텍스트 검색을 실행하여 앨범 제목을 찾을 수 있도록 이전 표를 수정하려면 TOKENIZE_FULLTEXT 함수를 사용하여 앨범 제목으로 토큰을 만듭니다. 그런 다음 TOKENLIST 데이터 유형을 사용하여 TOKENIZE_FULLTEXT에서의 토큰화 출력을 저장하는 열을 만듭니다. 이 예시에서는 AlbumTitle_Tokens 열을 만듭니다.

GoogleSQL

ALTER TABLE Albums
  ADD COLUMN AlbumTitle_Tokens TOKENLIST
  AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN;

PostgreSQL

ALTER TABLE albums
  ADD COLUMN albumtitle_tokens spanner.tokenlist
    GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN;

다음 예시에서는 CREATE SEARCH INDEX DDL을 사용하여 AlbumTitle 토큰(AlbumTitle_Tokens)에 대한 검색 색인(AlbumsIndex)을 만듭니다.

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

PostgreSQL

이 예시에서는 CREATE SEARCH INDEX을 사용합니다.

CREATE SEARCH INDEX albumsindex ON albums(albumtitle_tokens);

검색 색인을 추가한 후 SQL 쿼리를 사용하여 검색 기준과 일치하는 앨범을 찾습니다. 예를 들면 다음과 같습니다.

GoogleSQL

SELECT AlbumId
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")

PostgreSQL

SELECT albumid
FROM albums
WHERE spanner.search(albumtitle_tokens, 'fifth symphony')

데이터 일관성

색인이 생성되면 Spanner는 일관성을 보장하기 위해 데이터를 자동으로 백필하는 프로세스를 수행합니다. 쓰기가 커밋되면 동일한 트랜잭션에서 색인이 업데이트됩니다. Spanner는 데이터 일관성 검사를 자동으로 실행합니다.

검색 색인 스키마 정의

검색 색인은 테이블의 하나 이상의 TOKENLIST 열에 정의됩니다. 검색 색인에는 다음과 같은 구성요소가 있습니다.

  • 기본 테이블: 색인이 필요한 Spanner 테이블입니다.
  • TOKENLIST: 색인이 필요한 토큰을 정의하는 열 모음입니다. 이러한 열의 순서는 중요하지 않습니다.

예를 들어 다음 문에서 기본 테이블은 Albums입니다. TOKENLIST 열은 AlbumTitle(AlbumTitle_Tokens) 및 Rating(Rating_Tokens)에 생성됩니다.

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  SingerId INT64 NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumTitle STRING(MAX),
  Rating FLOAT64,
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN,
  Rating_Tokens TOKENLIST AS (TOKENIZE_NUMBER(Rating)) HIDDEN
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  singerid bigint NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumtitle character varying,
  rating double precision,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN,
  rating_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(rating)) VIRTUAL HIDDEN,
PRIMARY KEY(AlbumId));

다음 CREATE SEARCH INDEX 문을 사용하여 AlbumTitleRating에 대한 토큰으로 검색 색인을 만듭니다.

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC

검색 색인에는 다음 옵션이 있습니다.

  • 파티션: 검색 색인을 분할하는 선택적 열 그룹입니다. 파티션을 나눈 색인을 쿼리하는 것이 파티션을 나누지 않은 색인을 쿼리하는 것보다 훨씬 효율적일 때가 많습니다. 자세한 내용은 파티션 검색 색인을 참조하세요.
  • 정렬 순서 열: 검색 색인에서 검색 순서를 설정하는 선택적 INT64 열입니다. 자세한 내용은 검색 색인 정렬 순서를 참조하세요.
  • 인터리브 처리: 보조 색인과 마찬가지로 검색 색인도 인터리브 처리할 수 있습니다. 인터리브 처리된 검색 색인은 쓰기 및 기본 테이블 조인에 더 적은 리소스를 사용합니다. 자세한 내용은 인터리브 처리된 검색 색인을 참고하세요.
  • 옵션 절: 검색 색인의 기본 설정을 재정의하는 키-값 쌍 목록입니다.

검색 색인의 내부 레이아웃

검색 색인의 내부 표현에서 중요한 요소는 docid입니다. 이 요소는 임의의 길이가 될 수 있는 기본 테이블의 기본 키를 스토리지에 효율적인 표현 방식으로 나타냅니다. 또한 CREATE SEARCH INDEX 문의 사용자 제공 ORDER BY 열에 따라 내부 데이터 레이아웃의 순서를 생성합니다. 64비트 정수 1개 또는 2개로 표시됩니다.

검색 색인은 내부적으로 2가지 수준의 매핑으로 구현됩니다.

  1. docid에 대한 토큰
  2. 기본 테이블 기본 키에 대한 docid

이러한 스키마를 사용하면 Spanner가 각 <token, document> 쌍에 대해 전체 기본 테이블 기본 키를 저장할 필요가 없으므로 상당한 스토리지를 절약할 수 있습니다.

두 수준의 매핑을 구현하는 두 가지 유형의 실제 색인이 있습니다.

  1. 파티션 키와 docid를 기본 테이블 기본 키에 매핑하는 보조 색인. 이전 섹션의 예시에서 {SingerId, ReleaseTimestamp, uid}{AlbumId}에 매핑합니다. 보조 색인은 또한 CREATE SEARCH INDEXSTORING 절에 지정된 모든 열을 저장합니다.
  2. 토큰 색인은 정보 검색에서 일반적으로 설명되는 반전 색인과 유사하게 토큰을 docid에 매핑합니다. Spanner는 검색 색인의 각 TOKENLIST에 대해 별도의 토큰 색인을 유지합니다. 논리적으로 토큰 색인은 각 파티션 내 각 토큰의 docid 목록(정보 검색에서는 게시물 목록이라고 함)을 유지합니다. 이 목록은 빠른 검색을 위해 토큰별로 정렬되며 목록 내에서는 정렬을 위해 docid가 사용됩니다. 개별 토큰 색인은 Spanner API를 통해 노출되지 않는 구현 세부정보입니다.

Spanner는 docid에 대해 다음 4가지 옵션을 지원합니다.

검색 색인 docid 동작
검색 색인에서 ORDER BY 절이 생략됨 {uid} Spanner는 각 행을 식별하기 위해 숨겨진 고유 값(UID)을 추가합니다.
ORDER BY column {column, uid} Spanner는 파티션 내에서 column 값이 동일한 행 간에 UID 열을 결정자로 추가합니다.

사용 참고사항:

  • 내부 UID 열은 Spanner API를 통해 노출되지 않습니다.
  • UID가 추가되지 않은 색인에서는 이미 존재하는 (파티션,정렬 순서)가 있는 행을 추가하는 트랜잭션이 실패합니다.

예를 들어 다음 데이터를 고려하세요.

AlbumId SingerId ReleaseTimestamp SongTitle
a1 1 997 Beautiful days
a2 1 743 Beautiful eyes

사전 정렬 열이 오름차순이라고 가정하면 SingerId로 파티션을 나눈 토큰 색인의 콘텐츠는 다음과 같이 토큰 색인 콘텐츠의 파티션을 나눕니다.

SingerId _token ReleaseTimestamp uid
1 beautiful 743 uid1
1 beautiful 997 uid2
1 743 uid1
1 eyes 997 uid2

검색 색인 샤딩

Spanner가 테이블을 분할하면 특정 기본 테이블 행의 모든 토큰이 동일한 분할에 포함되도록 검색 색인 데이터를 배포합니다. 즉, 검색 색인은 문서 샤딩됩니다. 이 샤딩 전략은 성능에 상당한 영향을 미칩니다.

  1. 각 트랜잭션이 통신하는 서버 수는 토큰 수 또는 색인이 생성된 TOKENLIST 열 수와 관계없이 일정하게 유지됩니다.
  2. 여러 조건부 표현식이 포함된 검색어는 각 분할에서 독립적으로 실행되므로 분산 조인과 관련된 성능 오버헤드를 방지할 수 있습니다.

검색 색인에는 두 가지 배포 모드가 있습니다.

  • 균일 샤딩(기본값). 균일 샤딩에서는 각 기본 테이블 행의 색인이 생성된 데이터가 파티션의 색인 분할에 무작위로 할당됩니다.
  • 정렬 순서 샤딩. 정렬 순서 샤딩에서는 각 기본 테이블 행의 데이터가 ORDER BY 열(즉, 사전 정렬 열)을 기반으로 파티션의 색인 분할에 할당됩니다. 예를 들어 내림차순 정렬 순서의 경우 정렬 순서 값이 가장 큰 모든 행이 파티션의 첫 번째 색인 분할에 표시되고 그 다음으로 큰 정렬 순서 값 그룹이 다음 분할에 표시됩니다.

이러한 샤딩 모드로 부하 집중 위험과 쿼리 비용 사이에 절충이 이루어집니다.

  • 검색 색인의 읽기 또는 쓰기 패턴으로 인해 부하 집중이 발생할 수 있는 경우 균일 샤딩 검색 색인을 사용하는 것이 좋습니다. 균일 샤딩은 분할에 읽기 및 쓰기 부하를 균등하게 분산하여 부하 집중을 완화하지만, 그 대신 쿼리 실행 중에 리소스 사용량이 증가할 수 있습니다. 균일 샤딩 검색 색인에서는 무작위로 분산된 데이터로 인해 쿼리가 파티션 내의 모든 분할을 읽어야 합니다. 균일하게 샤딩된 색인에 액세스할 때 Spanner는 전체 쿼리 지연 시간을 줄이기 위해 모든 분할을 동시에 읽습니다.
  • 읽기 또는 쓰기 패턴으로 인해 부하 집중이 발생할 가능성이 낮은 경우 정렬 순서 샤딩 검색 색인을 사용하는 것이 좋습니다. 이 접근 방식을 사용하면 ORDER BY가 색인의 ORDER BY와 일치하고 비교적 낮은 LIMIT를 지정하는 쿼리의 비용을 줄일 수 있습니다. 이러한 쿼리를 실행할 때 Spanner는 파티션의 첫 번째 분할부터 점진적으로 읽으며, LIMIT가 조기에 충족될 수 있는 경우 모든 분할을 읽지 않고도 쿼리를 완료할 수 있습니다.

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC
OPTIONS (sort_order_sharding = true);

PostgreSQL

검색 색인의 샤딩 모드는 WITH 절을 사용하여 구성됩니다.

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC
WITH (sort_order_sharding = true);

sort_order_sharding=false가 설정되거나 지정되지 않은 경우 검색 색인은 균일한 샤딩을 사용하여 생성됩니다.

인터리브 처리된 검색 색인

보조 색인과 마찬가지로 기본 테이블의 상위 테이블에서 검색 색인을 인터리브 처리할 수 있습니다. 인터리브 처리된 검색 색인을 사용하는 주된 이유는 작은 파티션의 기본 테이블 데이터를 색인 데이터와 함께 배치하기 위함입니다. 이러한 상황별 공동 배치에는 다음과 같은 이점이 있습니다.

  • 쓰기가 2단계 커밋을 실행할 필요가 없습니다.
  • 검색 색인과 기본 테이블의 백 조인이 배포되지 않습니다.

인터리브 처리된 검색 색인에는 다음과 같은 제한사항이 있습니다.

  1. 정렬 순서 샤딩된 색인만 인터리브 처리할 수 있습니다.
  2. 하위 테이블이 아닌 최상위 테이블에서만 검색 색인을 인터리브 처리할 수 있습니다.
  3. 인터리브 처리된 테이블 및 보조 색인과 마찬가지로 상위 테이블의 키를 인터리브 처리된 검색 색인의 PARTITION BY 열의 프리픽스로 만듭니다.

인터리브 처리된 검색 색인 정의

다음 예시에서는 인터리브 처리된 검색 색인을 정의하는 방법을 보여줍니다.

GoogleSQL

CREATE TABLE Singers (
  SingerId INT64 NOT NULL
) PRIMARY KEY(SingerId);

CREATE TABLE Albums (
  SingerId INT64 NOT NULL,
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN
) PRIMARY KEY(SingerId, AlbumId),
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
PARTITION BY SingerId,
INTERLEAVE IN Singers
OPTIONS (sort_order_sharding = true);

PostgreSQL

CREATE TABLE singers(
  singerid bigint NOT NULL
PRIMARY KEY(singerid));

CREATE TABLE albums(
  singerid bigint NOT NULL,
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist
  GENERATED ALWAYS
AS (
  spanner.tokenize_fulltext(albumtitle)
) VIRTUAL HIDDEN,
  PRIMARY KEY(singerid, albumid)),
INTERLEAVE IN PARENT singers ON DELETE CASCADE;

CREATE
  SEARCH INDEX albumsindex
ON
  albums(albumtitle_tokens)
  PARTITION BY singerid INTERLEAVE IN singers WITH(sort_order_sharding = true);

검색 색인 정렬 순서

검색 색인 정렬 순서 정의 요구사항은 보조 색인과 다릅니다.

예를 들어 다음 테이블을 살펴보세요.

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumName STRING(MAX),
  AlbumName_Token TOKENLIST AS (TOKEN(AlbumName)) HIDDEN
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumname character varying,
  albumname_token spanner.tokenlist
      GENERATED ALWAYS AS(spanner.token(albumname)) VIRTUAL HIDDEN,
PRIMARY KEY(albumid));

애플리케이션은 ReleaseTimestamp순으로 정렬된 AlbumName을 사용하여 정보를 조회하는 보조 색인을 정의할 수 있습니다.

CREATE INDEX AlbumsSecondaryIndex ON Albums(AlbumName, ReleaseTimestamp DESC);

이에 상응하는 검색 색인은 다음과 같습니다(보조 색인에서 전체 텍스트 검색을 지원하지 않으므로 검색 색인은 일치검색 토큰화를 사용함).

CREATE SEARCH INDEX AlbumsSearchIndex
ON Albums(AlbumName_Token)
ORDER BY ReleaseTimestamp DESC;

검색 색인 정렬 순서는 다음 규칙을 준수해야 합니다.

  1. 검색 색인 정렬 순서에는 INT64 열만 사용합니다. Spanner는 모든 토큰 옆에 docid를 저장해야 하므로 임의 크기의 열은 검색 색인에서 너무 많은 리소스를 사용합니다. 특히 TIMESTAMP는 64비트 정수에 맞지 않는 나노초 정밀도를 사용하므로 정렬 순서 열에서는 TIMESTAMP 유형을 사용할 수 없습니다.
  2. 정렬 순서 열은 NULL이면 안 됩니다. 이 요구사항을 충족하는 방법에는 두 가지가 있습니다.

    1. 정렬 순서 열을 NOT NULL로 선언합니다.
    2. NULL 값이 제외되도록 색인을 구성합니다.

타임스탬프는 정렬 순서를 결정하는 데 사용되는 경우가 많습니다. 일반적인 방법은 이러한 타임스탬프에 유닉스 시간 이후 마이크로초를 사용하는 것입니다.

애플리케이션은 일반적으로 내림차순으로 정렬된 검색 색인을 사용하여 먼저 최신 데이터를 검색합니다.

NULL 필터링 검색 색인

검색 색인은 WHERE column_name IS NOT NULL 구문을 사용하여 기본 테이블 행을 제외할 수 있습니다. NULL 필터링은 파티셔닝 키, 정렬 순서 열, 저장된 열에 적용할 수 있습니다. 저장된 배열 열에 대한 NULL 필터링은 허용되지 않습니다.

예시

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
STORING (Genre)
WHERE Genre IS NOT NULL

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens)
INCLUDE (genre)
WHERE genre IS NOT NULL

쿼리에서 WHERE 절에 NULL 필터링 조건(이 예시의 경우 Genre IS NOT NULL)을 지정해야 합니다. 그렇지 않으면 쿼리 최적화 도구에서 검색 색인을 사용할 수 없습니다. 자세한 내용은 SQL 쿼리 요구사항을 참조하세요.

생성된 열에서 NULL 필터링을 사용하여 임의의 기준에 따라 행을 제외합니다. 자세한 내용은 생성된 열을 사용하여 부분 색인 만들기를 참조하세요.

다음 단계