테드 레빗
사람들은 4키 인덱스를 원하지 않습니다. 4ms 응답이 필요합니다.
애플리케이션 개발은 까다롭습니다. 각 애플리케이션은 고객을 대신하여 올바른 제품 또는 올바른 양식 검색, 주문, 취소, 배송, 상태 확인 등의 작업을 진행해야 합니다.
일반적인 쿼리 및 B-Tree 기반 데이터베이스와 인프라는 많은 모듈을 개발하는 데 적합합니다. 하지만 SQL 및 B-Tree 기반 검색이 SLA를 충족하는 데 효과적이지 않은 경우가 있습니다.
다음은 SQL 개발에서 직면할 수 있는 7가지 문제와 검색 기술을 사용한 해결 방법입니다. 여기서는 SQL 구현 예제로 Couchbase N1QL을, (논리적으로 말하면) B-Tree 기반 인덱스 샘플로 Couchbase GSI를, 검색 기술 예제로 FTS(Couchbase Full-Text Search)를 사용했습니다.
또한 여행 샘플 모델과 예제에서 Couchbase 샘플 데이터 세트의 데이터를 사용합니다.
개발자 과제
1. 이름 검색 문제(일명 '좋아요' 문제)
소셜 미디어는 다음을 좋아할 수 있습니다. 같은 를 자주 사용하지만 SQL은 그렇지 않습니다. 좋아요 술어는 쿼리에서 사용될 때 가장 먼저 살펴봐야 할 것 중 하나입니다. 신중하게 사용하지 않으면 LIKE 술어를 사용하면 지연 시간이 길어지고 처리량이 감소하며 작업이 중단될 수 있습니다. 다음은 실제 고객 애플리케이션에서 본 LIKE 술어 샘플입니다.
- WHERE 이름은 "%joe%"와 같습니다;
- WHERE UPPER(이름)이 "%JOE%"와 같은 경우
- WHERE REGEX_CONTAINS(name, ".*joe.*")
- "% joe %" 같은 이름
문제:
- 이름에 대한 색인이 있더라도 전체 색인을 스캔하여 joe가 포함된 모든 문서를 확인해야 합니다.
- 기능 인덱스(예: UPPER(name))를 생성하고 이름의 정확한 대소문자를 저장하려면 이름과 함께 추가 키를 가져야 합니다. 예: 맥도날드
- 처음 세 개의 술어를 사용하면 joe뿐만 아니라 joel, sojoel, bejoel 등의 문자열도 얻을 수 있습니다.
2. 멀티 매치 문제
이름이 이름, 중간 이름, 성으로 나뉘는 경우 모든 이름을 검색해야 합니다. 때로는 이름 중 하나에서 문자열을 검색하고 싶을 때도 있습니다. 이를 발신 번호와 일치시키는 것과 결합하세요. 쿼리는 다음과 같은 형태가 됩니다.
SELECT c.customerid, c.fname, c.mname, c.lname, c.address
고객 C로부터
WHERE (c.fname in ['John', 'Doe'])
또는 ['John', 'Doe']의 c.mname
또는 ['John', 'Doe']의 c.lname)
또는 (c.contacts의 모든 p가 p = '6509264813'를 만족함)
또는 (c.services의 모든 q가 q = '6509264813'를 충족)
이제 원하는 짧은 지연 시간뿐만 아니라 높은 처리량을 제공하는 B-TREE 인덱스를 생성해 보세요.
3. 정확히 일치하는 것 그 이상
실제 경기는 정확하지 않습니다. William은 다음과 같이 축약할 수 있습니다. 윌, 윌리, 빌, 빌리. 아일랜드어와 스코틀랜드어에는 추가 약어가 있습니다. 빌리를 검색하면 어떻게 윌리엄이 나오나요?
4. 비즈니스 요구 사항에 따른 검색
각 비즈니스에는 특정 고객에 대한 고객 정보(이름, 주소, 생년월일) 확인에 대한 규칙이 있습니다. 어떤 규칙은 이름이나 주소에 더 높은 우선순위를 부여하기도 합니다. 규칙에 따라 4개 중 2개는 정확히 일치해야 하고 나머지는 부분적으로 일치해야 할 수도 있습니다. N1QL 술어는 두 번째 경우보다 훨씬 더 복잡해집니다. 따라서 쿼리에 적합한 GSI 인덱스도 복잡해집니다.
5. 관련성 순서
N1QL의 순서는 ORDER BY 절과 그 안에 있는 표현식을 기반으로 합니다. 정확하지 않은 일치를 수행하는 경우 텍스트 점수와 주어진 검색 패턴과의 거리를 기준으로 순서를 지정하고 싶을 수 있습니다.
6. 배열 문제.
JSON 배열을 사용하면 1:N, N:M 관계를 객체 배열 또는 다른 객체에 대한 링크가 있는 배열로 저장할 수 있습니다. 이 문서 는 배열 조작에 대한 자세한 내용을 제공합니다. 배열의 두 번째 기사 는 배열에 대한 추가적인 집합 지향 연산에 대해 설명합니다. 배열 내부를 검색하는 동안 최상의 성능을 얻으려면 다음을 수행해야 합니다. 배열 키로 인덱스 생성. 배열 인덱스에는 각 배열 인덱스당 하나의 배열 키만 가질 수 있다는 제한이 있습니다. 따라서 여러 배열 필드가 있는 고객 개체가 있는 경우 단일 인덱스를 사용하여 모든 필드를 검색할 수 없습니다. 연락처 번호 배열과 이메일 연락처 배열이 있는 고객이 있는 경우 단일 인덱스를 사용하여 둘 다 검색할 수 없으므로 쿼리 비용이 많이 듭니다. 이것은 B-TREE 기반 배열 인덱스의 일반적인 제한 사항. B-트리 인덱스는 개별 배열 요소를 별개의 키로 저장하므로 배열 키가 여러 개 있으면 인덱스에 키 수(위의 예에서는 (연락처 수) * (이메일 수)의 곱을 저장해야 합니다. 그러면 인덱스 크기가 기하급수적으로 증가하게 됩니다. 이를 방지하기 위해 제품에서는 단일 인덱스에 대해 여러 개의 배열 키를 만들 수 없도록 설정되어 있습니다.
하지만 여러 JSON 배열을 효율적으로 조회하는 개발자의 작업은 여전히 남아 있습니다.
7. 여러 쿼리, 단일 인덱스.
JSON 모델은 여러 소스의 정보를 집계하는 데 성공적으로 사용되어 왔습니다. 이 모든 정보를 집계하는 목적은 단순히 저장하는 것이 아니라 쿼리하여 비즈니스 가치를 얻는 데 있습니다. 데이터 세트에 대한 임시 쿼리가 있는 경우, 쿼리에 필요한 인덱스를 미리 계획하고 다시 생성하는 것은 어렵습니다. N1QL 자체는 적응형 인덱스 메서드를 사용하면 특정 사용 사례에 도움이 됩니다. 각각 큰 결과 집합을 반환할 수 있는 여러 개의 술어가 있는 경우, 적응형 인덱스는 인덱스에서 쿼리 노드로의 데이터 전송량과 큰 중간 결과 집합으로 인해 성능 문제가 발생할 수 있습니다.
이 경우 개발자는 지속적으로 인덱스를 모니터링하고 조정해야 합니다. 성능 문제가 있는 사용자들은 계속해서 불만을 제기할 것입니다.
솔루션:
1. 이름 검색 문제(일명 '좋아요' 문제)
죠, 조엘, 졸리 등을 검색할 때 모두 개별 단어로 검색하는 경향이 있습니다. 일반적으로 la를 검색하는 것이 아니라joella. 검색 색인은 필드의 각 단어(이름, 제목 또는 댓글)를 가져와 언어 특성에 따라 분석하고 단어를 중지 단어(흥미롭지 않고 일반적으로 검색되지 않는 단어 - 예: 및, 또는, 또는, 영어의 경우)와 용어로 분류합니다. 실제 단어 대신 용어를 색인하면 두 가지 이점이 있습니다.
- 관련 단어를 함께 그룹화합니다: 프란시스, 프란시스코, frank, francois등 프란시스코를 검색하면 밀접하게 연관된 단어도 찾을 수 있습니다. 프란시스.
- 이렇게 그룹화하면 해당 단어의 모든 수정어가 아닌 루트 용어만 색인하면 되므로 색인의 크기도 줄어듭니다.
이름 검색으로 돌아갑니다: 이제 호텔 이름을 검색해 보겠습니다.
- 다음과 같은 유형의 문서에 FTS 색인을 만듭니다. 'hotel' 그리고 필드 이름 를 문서에 추가합니다.
- 이름 검색을 시작합니다:
- 실행할 쿼리는 다음과 같습니다:
curl -XPOST -H "Content-Type: application/json" \ http://172.23.120.38:8094/api/index/trhotelname/query \ -d '{ "explain": true, "fields": [ "*" ], "highlight": {}, "query": { "query": "francisco" } }'
- 검색 프란시스코 를 누르면 호텔 이름이 표시됩니다:
-
- "이름": "더 오팔 샌프란시스코",
- "이름": "프란시스코 베이 인",
- 등(총 11개 결과).
-
2. 멀티 매치 문제
앞서 복잡한 쿼리를 보았습니다.
SELECT c.customerid, c.fname, c.mname, c.lname, c.address
고객 C로부터
WHERE (c.fname in ['John', 'Doe'])
또는 ['John', 'Doe']의 c.mname
또는 ['John', 'Doe']의 c.lname)
또는 (c.contacts의 모든 p가 p = '6509264813'를 만족함)
또는 (c.services의 모든 q가 q = '6509264813'를 충족)
FTS에서는 다음과 같이 정의할 수 있습니다. 특별 _모든 필드 여러 필드를 결합하는 색인입니다. 이 인덱스에서 검색하면 모든 필드에서 자동으로 검색됩니다. (또한 이 문서) 이것은 호텔 문서의 5개 필드에 대한 색인입니다:
- 도시
- 국가
- 이름
- public_likes(배열 필드)
- 설명
다섯 가지 필드에 대한 예시 값입니다:
[
{
"도시": "Medway",
"국가": "영국",
"설명": "길링햄에서 약 3마일 떨어진 곳에 위치한 40개 침대 규모의 여름 호스텔로, 반 전원적인 분위기의 독특한 개조된 오스트 하우스에 자리하고 있습니다.",
"이름": "메드웨이 유스호스텔",
"public_likes": [
"줄리어스 트롬프 1세",
"코린 힐",
"재든 맥켄지",
"발리 라이언",
"브라이언 킬백",
"릴리안 맥러플린",
"모세 피니 양",
"엘노라 트란토우"
]
}
]
이러한 모든 필드에 검색 인덱스를 정의한 후에는 쿼리를 시작하기만 하면 됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
curl -XPOST -H "콘텐츠 유형: 애플리케이션/json" \ http://172.23.120.38:8094/api/index/trinfoall/query \ -d '{ "설명": true, "필드": [ "*" ], "하이라이트": {}, "query": { "query": "summer" } }' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
curl -XPOST -H "콘텐츠 유형: 애플리케이션/json" \ http://172.23.120.38:8094/api/index/trinfoall/query \ -d '{ "설명": true, "필드": [ "*" ], "하이라이트": {}, "query": { "query": "메드웨이 \"발리 라이언\" 영국" } }' |
여기에 사용된 _all 필드 인덱스에 대한 인덱스 정의는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
{ "type": "전체 텍스트 인덱스", "name": "trinfoall", "uuid": "430401ce6b5c1879", "sourceType": "couchbase", "sourceName": "travel-sample", "sourceUUID": "ddd78a53e740e6a8274e13c990b47abc", "planParams": { "최대 파티션당 인덱스": 171 }, "params": { "doc_config": { "docid_prefix_delim": "", "docid_regexp": "", "모드": "type_field", "type_field": "type" }, "매핑": { "분석": {}, "기본_분석기": "표준", "기본_날짜_파서": "날짜/시간 옵션", "default_field": "_all", "default_mapping": { "동적": true, "enabled": false }, "default_type": "_default", "docvalues_dynamic": true, "index_dynamic": true, "store_dynamic": false, "type_field": "_유형", "types": { "호텔": { "동적": true, "enabled": true, "속성": { "city": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "city", "type": "text" } ] }, "country": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "country", "type": "text" } ] }, "설명": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "설명", "type": "text" } ] }, "name": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "name", "type": "text" } ] }, "public_likes": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "public_likes", "type": "text" } ] } } } } }, "store": { "indexType": "scorch", "kvStoreName": "" } }, "sourceParams": {} } |
3. 정확히 일치하는 것 그 이상
검색(FTS)은 정확한 검색 이상의 기능을 수행합니다. 용어 검색, 범위 검색, 퍼지 검색, 접속사, 접속사 해제, 지리적 검색(가장 가까운 이웃), 정규식 검색, 부스트 검색, 구문 검색 등을 수행합니다. 다음에서 예제와 자세한 내용을 참조하세요. 카우치베이스 FTS 문서.
4. 비즈니스(사용자) 요구사항에 따른 검색
뉴욕에서 호텔을 검색할 때 맨해튼 호텔을 선호하지만 다른 호텔도 보고 싶을 수 있습니다. 이제 맨해튼 검색어에 ^5를 추가하여 부스트하면 됩니다. 이렇게 부스트하면 맨해튼이 포함된 문서의 점수가 향상됩니다. 결과는 기본적으로 점수가 낮은 순서로 정렬됩니다.
5. 관련성 순서
SQL에서 순서는 표현식 또는 필드 자체의 값을 기준으로 합니다. 검색에서 문서의 관련성은 검색 대상과 문서에 포함된 내용 사이의 거리로 계산됩니다. 이것은 이전 섹션에서 용어의 중요도를 높여서 조작한 점수입니다. 사용자는 결과 정렬 검색 요청의 정렬 절을 사용하여 이 점수 및 기타 필드로 검색할 수 있습니다.
6. 배열 문제
이제 다음 네 개의 배열에 단일 인덱스를 만들어 보겠습니다.
- 공개_좋아요
- reviews.author
- reviews.ratings.Location
- reviews.ratings.Service
인덱스 정의는 다음과 같습니다. 이것은 다음의 단일 인덱스입니다. 4개의 배열 키. 이는 B-트리 기반 인덱스에서는 절대 할 수 없는 작업입니다.
이제 위의 필드 중 하나 이상에서 술어를 사용하여 쿼리할 수 있습니다.
예 1: '발리 라이언'이 좋아하거나 서비스 평점 4점 이상인 호텔
‘{
"설명": true,
"필드": [ "*" ],
"하이라이트": {},
"query": { "query": "+public_likes:\"발리 라이언\" reviews.ratings.Service:>4″ }
}’
전체 인덱스 정의.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
{ "type": "전체 텍스트 인덱스", "name": "trmultiarrayidx", "uuid": "7b3a85992989e196", "sourceType": "couchbase", "sourceName": "travel-sample", "sourceUUID": "ddd78a53e740e6a8274e13c990b47abc", "planParams": { "최대 파티션당 인덱스": 171 }, "params": { "doc_config": { "docid_prefix_delim": "", "docid_regexp": "", "모드": "type_field", "type_field": "type" }, "매핑": { "분석": {}, "기본_분석기": "표준", "기본_날짜_파서": "날짜/시간 옵션", "default_field": "_all", "default_mapping": { "동적": true, "enabled": false }, "default_type": "_default", "docvalues_dynamic": true, "index_dynamic": true, "store_dynamic": false, "type_field": "_유형", "types": { "호텔": { "동적": false, "enabled": true, "속성": { "public_likes": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "public_likes", "type": "text" } ] }, "reviews.author": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "reviews.author", "type": "text" } ] }, "reviews.ratings.Location": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "reviews.ratings.Location", "type": "text" } ] }, "reviews.ratings.Service": { "동적": false, "enabled": true, "fields": [ { "docvalues": true, "IN_IN_ALL": true, "include_term_vectors": true, "index": true, "name": "reviews.ratings.Service", "type": "text" } ] } } } } }, "store": { "indexType": "scorch", "kvStoreName": "" } }, "sourceParams": {} } |
7. 다중 쿼리, 단일 인덱스.
"때때로 쿼리는 초콜릿 한 상자와 같습니다. 어떤 쿼리가 나올지 알 수 없기 때문입니다." 애드혹 쿼리를 실행할 수 있는 비즈니스 사용자에게 데이터를 노출하려는 경우, 모든 종류의 쿼리 속도를 높이기 위해 모든 종류의 인덱스를 만들 수는 없습니다. 성능 튜닝에는 다른 접근 방식이 필요합니다.
위의 섹션 (2)에서 5개의 필드에 대해 생성한 인덱스를 고려하세요:
- 도시
- 국가
- 이름
- public_likes(배열 필드)
- 설명
B-Tree(카우치새의 경우 GSI 인덱스)를 생성하면 다음과 같은 모양이 됩니다:
인덱스 생성 itravel ON 여행 샘플
(도시, 국가, 이름, DISTINCT public_likes, 설명) WHERE type = 'hotel';
이를 통해 이점을 얻을 수 있는 쿼리는 다음과 같습니다. 규칙 설명. 가장 큰 문제는 각 쿼리 블록(특히 인덱스 스캔)이 인덱스의 선행 키에 술어를 사용해야 한다는 것입니다. 이것은 인덱스의 효율성을 제한합니다.
위에서 생성한 검색(FTS) 인덱스를 사용하면 접속사, 분리사, 필수 등을 조합하여 어떤 조합으로도 쿼리할 수 있습니다. 복잡한 쿼리의 경우 검색에 몇 밀리초 이상 걸릴 수 있지만, 이 모든 것을 하나의 인덱스에서 처리할 수 있습니다. 리소스를 합리적으로 사용할 수 있는 유연성 덕분에 검색 인덱스의 가치는 매우 높습니다.
참고: 곧 출시될 Couchbase 6.5 버전에서는 검색 색인을 사용하여 더 쉽게 쿼리할 수 있도록 개선되었습니다. 이 글에서 Binh Le가 이에 대해 설명했습니다:https://www.couchbase.com/blog/n1ql-and-search-how-to-leverage-fts-index-in-n1ql-query/
참조
- 카우치베이스 문서: https://docs.couchbase.com/server/6.0/fts/full-text-intro.html
- 카우치베이스 검색 리소스: https://www.couchbase.com/products/full-text-search
- 카우치베이스 FTS 온라인 교육: https://learn.couchbase.com/store/509465-cb121-intro-to-couchbase-full-text-search-fts
- 카우치베이스 블로그: https://www.couchbase.com/blog/category/full-text-search/
- 카우치베이스와 몽고DB 텍스트 검색 비교: https://www.couchbase.com/blog/searching-json-comparing-text-search-in-couchbase-and-mongodb/