들어가며
이전에 회사 업무에 MongoDB Atlas Search를 도입하며 찍먹용 정리글을 작성했던게 있어 블로그에 업로드 하려한다.
ES 같은 검색엔진을 구성하는 것도 방법이었지만, DB를 MongoDB를 사용하고 있었고, 적은 비용으로 DB와 검색엔진을 모두 사용할 수 있어서 편하게 개발했던 기억이 난다.
물론 지금도 서비스에서 잘 사용중이다 🚀
Atlas Search 개요
MongoDB의 Atlas Search를 사용하면 Atlas 클러스터의 데이터에 대한 세분화된 텍스트 인덱스 및 쿼리를 수행할 수 있다.
데이터베이스와 함께 추가 관리나 별도의 검색 시스템 없이 애플리케이션에 대한 고급 검색 기능을 사용할 수 있다.
Atlas Search는 여러 종류의 텍스트 분석기와 $search, $searchMeta와 같은 Atlas Search 집계 파이프라인 단계를 다른 MongoDB 집계 파이프라인 단계와 함께 사용하는 풍부한 쿼리 언어, 점수 기반 결과 순위를 위한 옵션을 제공함.
사용하려면 Search Index를 반드시 생성해야한다.
주요 기능
- Full-text Search
- 형태소 분석, 불용어, 동의어를 포함한 전체 텍스트 검색 제공
- Sync
- ElasticSearch 와 달리 데이터 베이스와 검색엔진 동기화를 구현할 필요 없이 바로 사용
- Scoring
- 검색어에 일치하는 순으로 scoring 가능
- 정확도순 결과를 쉽게 알 수 있음
- Fuzzy
- 오타나 철자 오류에 상관없이 연관성 높은 검색 결과를 반환
- ex. Tim Cook 데이터 검색 시 Timothy Cook, Yim Cook 등 철자 오류, 약간 다르게 검색해도 fuzzy 알고리즘에 따라 검색 결과로 나오게 할 수 있음.
- Synonyms
- 동의어 매핑 시 동일한 의미를 가진 데이터라면 결과에 반환
- Autocomplete
- 검색 자동완성 기능 제공
- 검색어 제안
string search vs full-text search
- string search(ex. regex, LIKE) 는 컬렉션의 모든 문서를 앞에서 부터 한글자씩 탐색해서 일치하는 문서를 반환함
- full-text search는 인덱싱과 Apache Lucene 라이브러리를 사용해 일종의 용어집을 만들어두고 검색 결과를 반환
- 데이터가 많아질 수록 string search에 비해 성능차이가 커짐
Indexing Process
- 인덱싱 준비 과정에서 데이터는 토큰화를 거침
- 토큰화 : 데이터를 단어, 구문, 기호 등의 의미있는 토큰으로 나누는 작업
- ex. eating, to eat, ate → 의미없는 어간을 지우고 eat 으로만 남게 됨

- 토큰화 이후 모든 데이터는 대소문자 중 하나만 사용되도록 변경됨
- 인덱싱 이후 결과적으로 토큰 단위로 나눠진 단어들과 데이터가 매핑되는 일종의 용어집이 생김

출처 : https://mongodb-developer.github.io/search-lab/docs/full-text-search/how-search-works
Atlas Search 시작
Search Index 생성
Atlas Search를 사용하여 Atlas 클러스터의 데이터를 쿼리하려면 Atlas Search 인덱스를 구성해야 함
단일 필드 또는 여러 필드에 대해 Atlas Search 인덱스를 만들 수 있다.
데이터를 정렬하거나 필터링하는 데 정기적으로 사용하는 필드를 인덱싱하는 것이 좋다.
기본적으로 아래와 같은 형태의 index이다.
{
"mappings": {
"dynamic": <boolean>,
"fields": {
"<field-name>": [
{
"type": "<field-type>",
...
},
...
],
...
}
}
}
정적 매핑 vs 동적 매핑
- Atlas Search Index 생성 시 매핑 방법을 정의할 수 있다.
- 정적 매핑
- 정적 매핑을 사용하여 동적으로 인덱싱하지 않을 필드의 인덱스 옵션을 설정하거나, 인덱스 내의 다른 필드와 독립적으로 단일 필드를 구성하고 싶을 때 활용
- 정적 매핑을 위해서는
mappings.dynamic을false로 설정하고mappings.fields를 사용하여 인덱싱할 필드를 지정 - 중첩된 필드의 인덱스를 정의할 때는 해당 중첩 필드의 각 상위 필드에 대한 매핑을 정의
- 동적 매핑보다 디스크 공간 차지가 적어 성능이 좋다. 인덱싱할 필드가 정해져 있다면 활용
- 정적 매핑 예시
{
"analyzer": "lucene.standard", // 기본 인덱스 분석기
"searchAnalyzer": "lucene.standard", // 검색 분석기
"mappings": {
"dynamic": false, // 정적 매핑
"fields": {
"address": {
"type": "document", // address 필드의 타입
"fields": {
"city": { // address.city 필드 정의
"type": "string",
"analyzer": "lucene.simple", // 분석기로 lucene.simple 사용
"ignoreAbove": 255 // 길이가 255byte 초과하는 데이터 무시
},
"state": { // address.state 필드 정의
"type": "string",
"analyzer": "lucene.english"
}
}
},
"company": { // company 필드 정의
"type": "string",
"analyzer": "lucene.whitespace", // 기본 analyzer 지정
"multi": {
"mySecondaryAnalyzer": { // multi analyzer 사용
"type": "string",
"analyzer": "lucene.french"
}
}
},
"employees": { // employees 필드 정의
// Atlas Search에는 배열 요소의 데이터 유형만 필요
"type": "string", // employees는 string[] 이나 인덱싱에서 따로 명시할 필요 없음
"analyzer": "lucene.standard"
}
}
}
}
- 동적 매핑
- 스키마가 정기적으로 변경되거나 알 수 없는 경우 또는 Atlas Search를 실험할 때 동적 매핑을 사용
- 동적 매핑을 사용하도록 전체 인덱스를 구성하거나,
document유형의 필드와 같은 개별 필드를 동적으로 매핑하도록 지정할 수 있음 mappings.dynamic을true로 설정- 동적으로 매핑된 인덱스는 정적으로 매핑된 인덱스보다 더 많은 디스크 공간을 차지하며 성능이 저하될 수 있음
- 정적, 동적 매핑 결합도 가능
- 예시
{
"analyzer": "lucene.standard",
"searchAnalyzer": "lucene.standard",
"mappings": {
"dynamic": false,
"fields": {
"company": {
"type": "string", // 정적 매핑
"analyzer": "lucene.whitespace",
"multi": {
"mySecondaryAnalyzer": {
"type": "string",
"analyzer": "lucene.french"
}
}
},
"employees": {
"type": "string", // 정적 매핑
"analyzer": "lucene.standard"
},
"address": {
"type": "document", // address 필드는 동적 매핑
"dynamic": true
}
}
}
}
Atlas Search Query
$searchoperator 를 사용해 검색 가능
[
{
$search: {
index: "default", // 사용할 search index 지정
text: {
query: "one bedroom rental", // 검색어
path: "name" // 검색할 필드 지정
}
}
}
]
- aggregation pipeline 안에서 다른 stage와 결합해 복잡한 쿼리 사용 가능
- 예시
- plot 필드에는 Hawaii 또는 Alaska가 포함되어야 합니다.
- plot 필드는 연도와 같은 4자리 숫자가 포함되어야 합니다.
- genres필드에는 Comedy 또는 Romance가 포함되어서는 안 됩니다.
- title 필드에는 Beach 또는 Snow가 포함되어서는 안 됩니다.
const MongoClient = require("mongodb").MongoClient;
const assert = require("assert");
const agg = [
{
$search: {
compound: {
must: [
{
text: {
query: ["Hawaii", "Alaska"],
path: "plot",
},
},
{
regex: {
query: "([0-9]{4})",
path: "plot",
allowAnalyzedField: true,
},
},
],
mustNot: [
{
text: {
query: ["Comedy", "Romance"],
path: "genres",
},
},
{
text: {
query: ["Beach", "Snow"],
path: "title",
},
},
],
},
},
},
{
$project: {
title: 1,
plot: 1,
genres: 1,
_id: 0,
},
},
];
MongoClient.connect(
"<connection-string>",
{ useNewUrlParser: true, useUnifiedTopology: true },
async function (connectErr, client) {
assert.equal(null, connectErr);
const coll = client.db("sample_mflix").collection("movies");
let cursor = await coll.aggregate(agg);
await cursor.forEach((doc) => console.log(doc));
client.close();
}
);
- 더 많은 사용법은 MongoDB Atlas Search Tutorial 참고 바람
Pagination
MongoDB 6.0.13+ 이상 클러스터에서 Atlas Search 사용 시 Pagination 과정에서 단순 offset이나 cursor 기반 이외에 Atlas Search에서 제공하는 searchSequenceToken 을 이용한 searchAfter , searchBefore 을 사용할 수 있다.
순차적인 pagination
ex. 무한스크롤처럼 사용자가 특정 페이지로 넘어가지 않는 페이지네이션
$skip 과 동일한 역할로 searchSequenceToken 사용
[{
"$search": {
"index": "<index-name>",
"<operator-name>"|"<collector-name>": {
<operator-specification>|<collector-specification>
}
...
},
{
"$project": {
**{ "paginationToken" : { "$meta" : "searchSequenceToken" }**
}
},
...
}]
- search 쿼리 $project 단계에서 기준점 = searchSequenceToken 을 함께 반환할 수 있다.
{
"_id": 65,
"name": "A large sunny bedroom",
"room_type": "Private room",
"paginationToken": "CEAV6V6pPyIDEIIB" // 이런 형식으로 반환됨 (playground에서 확인)
},
searchSequenceToken은 결과의 각 문서에 대해 기본 64 인코딩된 토큰을 생성searchAfter- 특정 참조 지점 이후를 검색하려면
searchAfter옵션과searchSequenceToken으로 생성된 토큰을 사용하여 참조 지점을 지정한다.
- 특정 참조 지점 이후를 검색하려면
{
"$search": {
"index": "<index-name>",
"<operator-name>"|"<collector-name>": {
<operator-specification>|<collector-specification>
},
"searchAfter": "<base64-encoded-token>", // 해당 token 이후부터 조회
...
},
"$project": {
{ "paginationToken" : { "$meta" : "searchSequenceToken" } }
},
...
}
searchBefore- 기준점 앞에서 Atlas Search를 하려면
searchSequenceToken에 의해 생성된 토큰과 함께searchBefore옵션을 사용하여$search쿼리에서 기준점을 지정
- 기준점 앞에서 Atlas Search를 하려면
{
"$search": {
"index": "<index-name>",
"<operator-name>"|"<collector-name>": {
<operator-specification>|<collector-specification>
},
"searchBefore": "<base64-encoded-token>", // 해당 token 이전에서 조회
...
},
"$project": {
{ "paginationToken" : { "$meta" : "searchSequenceToken" } }
},
...
}
- 실제 aggregate pipeline 예시
- 지정된 기준점 이후
title필드의war이라는 용어가 포함된 문서 10개를 요청
- 지정된 기준점 이후
db.movies.aggregate([
{
"$search": {
"text": {
"path": "title",
"query": "war"
},
"sort": {score: {$meta: "searchScore"}, "released": 1},
"searchAfter": "CMtJGgYQuq+ngwgaCSkAjBYH7AAAAA=="
}
},
{
"$limit": 10
},
{
"$project": {
"_id": 0,
"title": 1,
"released": 1,
"paginationToken" : { "$meta" : "searchSequenceToken" },
"score": { "$meta": "searchScore" }
}
}
])
- 지정된 기준점 이전
title필드의war이라는 용어가 포함된 문서 10개를 요청- 주의: 역순으로 반환됨
db.movies.aggregate([
{
"$search": {
"text": {
"path": "title",
"query": "war"
},
"sort": {score: {$meta: "searchScore"}, "released": 1},
**"searchBefore": "CJ6kARoGELqvp4MIGgkpACDA3U8BAAA="**
}
},
{
"$limit": 10
},
{
"$project": {
"_id": 0,
"title": 1,
"released": 1,
"paginationToken" : { "$meta" : "searchSequenceToken" },
"score": { "$meta": "searchScore" }
}
}
])
순차적이지 않은 pagination
ex. 2페이지에서 5페이지로 건너뛰는 페이지네이션
결과의 특정 페이지로 이동하려면 $skip 및 $limit를 searchAfter 또는 searchBefore 옵션과 결합한다.
- 시나리오:
searchAfter및$skip을 사용하여 2페이지에서 5페이지로 이동searchSequenceToken에서 생성된 토큰을 사용하여 결과를 검색한 다음 결과에서 문서 20개를 건너뛸 참조 지점을 지정- 10개씩 반환하는 페이지네이션이라 가정했을때,
- 2페이지 마지막 sequenceToken을 기준점으로 지정
- 2페이지와 5페이지 사이에는 3, 4페이지 2개가 존재
- 3, 4 페이지 각각 10개씩 총 20개의 데이터가 존재함
- 따라서 기준점 이후부터 20개를 skip 한 후 데이터를 반환하면 5페이지 결과가 나온다.
- $skip은 많은 양을 건너뛸 수록 느려지기 때문에 skip만 사용해 건너뛸 때보다 성능 측면에서 훨씬 효율적이다.
db.movies.aggregate([
{
"$search": {
"index": "pagination-tutorial",
"text": {
"path": "title",
"query": "summer"
},
"searchAfter": "COwRGgkpAPQV0hQAAAA=", // 이 지점 이후에서 조회
"sort": { "released": 1 }
}
},
{
"$skip": 20 // 2 -> 5 사이에 3, 4 페이지 = 2개 * 10 = 20개
},
{
"$limit": 10 // 10개씩 반환
},
{
"$project": {
"_id": 0,
"title": 1,
"released": 1,
"genres": 1,
"paginationToken" : { "$meta" : "searchSequenceToken" },
"score": { "$meta": "searchScore" }
}
}
])
Autocomplete
입력 시 실시간 검색 애플리케이션에서 autocomplete 연산자를 사용하면 애플리케이션의 검색 필드에 문자가 입력될 때 단어를 더욱 정확하게 예측할 수 있다.
자동완성을 위해 autocomplete index를 구성해야 함
{
$search: {
"index": "<index name>", // optional, defaults to "default"
"autocomplete": {
"query": "<search-string>",
"path": "<field-to-search>",
"tokenOrder": "any|sequential",
"fuzzy": <options>,
"score": <options>
}
}
}
- token화 전략에 따라 검색 결과가 달라질 수 있다.
Tokenization
| 전략 | 설명 |
|---|---|
edgeGram |
분석기로 구분된 단어의 왼쪽에서 시작하는 가변 길이 문자 시퀀스를 사용하여 grams 라고 불리는 인덱스 가능 토큰을 생성 |
rightEdgeGram |
분석기로 구분된 단어의 오른쪽에서 시작하는 가변 길이 문자 시퀀스에서 grams 라고 불리는 인덱스 가능한 토큰을 생성 |
nGram |
단어 위로 가변 길이 문자 창을 밀어서 grams 이라고 하는 인덱스 생성 가능 토큰 생성edgeGram 또는 rightEdgeGram보다 nGram 에 대해 더 많은 토큰을 생성 필드를 인덱스하는 데 더 많은 공간과 시간이 소요, nGram 은 긴 복합어가 포함된 언어나 공백을 사용하지 않는 언어를 쿼리하는 데 더 적합 |
예시
// title field에서 "off" 검색
const agg = [
{$search: **{autocomplete: {query: "off", path: "title"}}},**
{$limit: 10},
{$project: {_id: 0,title: 1}}
];
// run pipeline
const result = await coll.aggregate(agg);
edgeGram- 왼쪽부터 검색
{ title: 'Off Beat' }
{ title: 'Off the Map' }
{ title: 'Off and Running' }
{ title: 'Hands off Mississippi' }
{ title: 'Taking Off' }
{ title: 'Noises Off...' }
{ title: 'Brassed Off' }
{ title: 'Face/Off' }
{ title: 'Benji: Off the Leash!' }
{ title: 'Set It Off' }
rightEdgeGram- 오른쪽부터 검색
{ title: 'Taking Off' }
{ title: 'Noises Off...' }
{ title: 'Brassed Off' }
{ title: 'Face/Off' }
{ title: 'Set It Off' }
{ title: 'Hands off Mississippi' }
{ title: "Ferris Bueller's Day Off" }
{ title: 'Off Beat' }
{ title: 'Benji: Off the Leash!' }
{ title: 'Off the Map' }
nGram- 다른 위치에서 나타남
- Coffee, Officer, Romanoff 등
{ title: 'Come Have Coffee with Us' }
{ title: 'A Spell to Ward Off the Darkness' }
{ title: 'Remake, Remix, Rip-Off: About Copy Culture & Turkish Pop Cinema' }
{ title: 'Benji: Off the Leash!' }
{ title: 'A Coffee in Berlin' }
{ title: 'An Officer and a Gentleman' }
{ title: 'The Official Story' }
{ title: "The Officer's Ward" }
{ title: 'Hands off Mississippi' }
{ title: 'Romanoff and Juliet' }
fuzzy
검색어 또는 용어와 유사한 문자열을 찾는다.
주로 검색어 철자에 오타가 있어도 최대한 검색이 가능하도록 하기 위해 사용
ex. Tim Cook을 Yim Cook 으로 검색해도 나오게끔
| 필드 | 설명 |
|---|---|
maxEdits |
쿼리를 문서의 단어와 일치시키기 위해 쿼리 문자열 pre에 하나의 문자 변형만 허용됨을 나타냅니다. |
prefixLength |
쿼리를 문서의 단어와 일치시킬 때 쿼리 문자열 pre 의 첫 번째 문자가 변경될 수 없음을 나타냅니다. |
maxExpansions |
쿼리 문자열을 문서의 단어와 일치시킬 때 pre에 대해 최대 256개의 유사한 용어를 고려할 수 있음을 나타냅니다. |
예시
// define pipeline
const agg = [
{$search: {
**autocomplete: {
query: "pre",
path: "title",
fuzzy: {
"maxEdits": 1,
"prefixLength": 1,
"maxExpansions": 256
}
}**
}},
{$limit: 10},
{$project: {_id: 0,title: 1}}
];
// run pipeline
const result = await coll.aggregate(agg);
edgeGram- 모든 제목의 단어 왼쪽에 첫 번째 문자 상수를 사용하여 한 글자 수정을 사용하여 쿼리 문자열에 대해 예측된 단어를 보여줌
- Perils, Poet, Private 등
{ title: 'The Perils of Pauline' }
{ title: 'The Blood of a Poet' }
{ title: 'The Private Life of Henry VIII.' }
{ title: 'The Private Life of Don Juan' }
{ title: 'The Prisoner of Shark Island' }
{ title: 'The Prince and the Pauper' }
{ title: 'The Prisoner of Zenda' }
{ title: 'Dance Program' }
{ title: 'The Pied Piper' }
{ title: 'Prelude to War' }
rightEdgeGram- 모든 제목의 단어 오른쪽에 첫 번째 문자 상수를 사용하여 한 글자 수정을 사용하여 쿼리 문자열에 대해 예측된 단어를 보여줌
- Where, People, Here 등
{ title: 'Where Are My Children?' }
{ title: 'The Four Horsemen of the Apocalypse' }
{ title: 'The Hunchback of Notre Dame' }
{ title: 'Show People' }
{ title: 'Berkeley Square' }
{ title: 'Folies Bergère de Paris' }
{ title: 'Here Comes Mr. Jordan' }
{ title: 'Cat People' }
{ title: 'People on the Alps' }
{ title: "The Gang's All Here" }
nGram- 제목에 있는 단어의 다른 위치에서 한 문자가 수정된 쿼리 문자열에 대해 예측된 단어를 보여줌
- vampires, Shphead, Apocalypse 등
{ title: 'The Perils of Pauline' }
{ title: 'Les vampires' }
{ title: 'The Saphead' }
{ title: 'The Four Horsemen of the Apocalypse' }
{ title: 'He Who Gets Slapped' }
{ title: 'The Phantom of the Opera' }
{ title: 'Show People' }
{ title: 'The Blood of a Poet' }
{ title: 'The 3 Penny Opera' }
{ title: 'Shanghai Express' }
Scoring
Atlas Search 쿼리가 반환하는 모든 문서에는 관련성에 따른 점수가 할당되고 결과 세트에 포함된 문서는 가장 높은 점수에서 낮은 점수 순으로 반환된다.
- 문서의 점수에 영향을 미치는 요소
- 문서에서 검색어의 위치,
- 문서에서 검색어의 발생 빈도
- 쿼리에서 사용하는 operator 유형
- analyzer 유형
예시
db.movies.aggregate([
{
"$search": {
"text": {
<operator-specification>
}
}
},
{
"$project": {
"<field-to-include>": 1,
"<field-to-exclude>": 0,
**"score": { "$meta": "searchScore" }**
}
}
])
- 정확도순 정렬을 원한다면 $project stage 이후 별도의 $sort 를 지정할 필요가 없다.
- Atlas Search는 가장 높은 점수부터 반환함
- 정확도순이 아닌 다른 정렬을 원한다면 아래 페이지 참고
Final Example
Atlas Search Playground를 통해 테스트 진행해 봄
- Origin Data
[
{
"_id": 1,
"name": "Ribeira Charming Duplex",
"accommodates": 8,
"room_type": "Entire home/apt",
"pricePerNight": 80,
"host_name": "Ana&Gonçalo",
"host_email": "anagonalo@gmail.com"
},
{
"_id": 2,
"name": "Horto flat with small garden",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 317,
"host_name": "Ynaie",
"host_email": "ynaie@gmail.com"
},
{
"_id": 3,
"name": "Ocean View Waikiki Marina w/prkg",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 115,
"host_name": "David",
"host_email": "david@gmail.com"
},
{
"_id": 4,
"name": "Private Room in Bushwick",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 40,
"host_name": "Josh",
"host_email": "josh@gmail.com"
},
{
"_id": 5,
"name": "Apt Linda Vista Lagoa - Rio",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 701,
"host_name": "Livia",
"host_email": "livia@gmail.com"
},
{
"_id": 6,
"name": "New York City - Upper West Side Apt",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 135,
"host_name": "Greta",
"host_email": "greta@gmail.com"
},
{
"_id": 7,
"name": "Copacabana Apartment Posto 6",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 119,
"host_name": "Ana Valéria",
"host_email": "ana@gmail.com"
},
{
"_id": 8,
"name": "Charming Flat in Downtown Moda",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 527,
"host_name": "Zeynep",
"host_email": "zeynep@gmail.com"
},
{
"_id": 9,
"name": "Catete's Colonial Big Hause Room B",
"accommodates": 8,
"room_type": "Private room",
"pricePerNight": 250,
"host_name": "Beatriz",
"host_email": "beatriz@gmail.com"
},
{
"_id": 10,
"name": "Modern Spacious 1 Bedroom Loft",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 50,
"host_name": "Konstantin",
"host_email": "konstantin@gmail.com"
},
{
"_id": 11,
"name": "Deluxe Loft Suite",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 205,
"host_name": "Mae",
"host_email": "mae@gmail.com"
},
{
"_id": 12,
"name": "Ligne verte - à 15 min de métro du centre ville.",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 43,
"host_name": "Caro",
"host_email": "caro@gmail.com"
},
{
"_id": 13,
"name": "Soho Cozy, Spacious and Convenient",
"accommodates": 3,
"room_type": "Entire home/apt",
"pricePerNight": 699,
"host_name": "Giovanni",
"host_email": "giovanni@gmail.com"
},
{
"_id": 14,
"name": "3 chambres au coeur du Plateau",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 140,
"host_name": "Margaux",
"host_email": "margaux@gmail.com"
},
{
"_id": 15,
"name": "Ótimo Apto proximo Parque Olimpico",
"accommodates": 5,
"room_type": "Entire home/apt",
"pricePerNight": 858,
"host_name": "Jonathan",
"host_email": "jonathan@gmail.com"
},
{
"_id": 16,
"name": "Double Room en-suite (307)",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 361,
"host_name": "Ken",
"host_email": "ken@gmail.com"
},
{
"_id": 17,
"name": "Nice room in Barcelona Center",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 50,
"host_name": "Anna",
"host_email": "anna@gmail.com"
},
{
"_id": 18,
"name": "Be Happy in Porto",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 30,
"host_name": "Fábio",
"host_email": "fbio@gmail.com"
},
{
"_id": 19,
"name": "City center private room with bed",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 181,
"host_name": "Yi",
"host_email": "yi@gmail.com"
},
{
"_id": 20,
"name": "Surry Hills Studio - Your Perfect Base in Sydney",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 181,
"host_name": "Ben",
"host_email": "ben@gmail.com"
},
{
"_id": 21,
"name": "Cozy house at Beyoğlu",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 58,
"host_name": "Ali",
"host_email": "ali@gmail.com"
},
{
"_id": 22,
"name": "Easy 1 Bedroom in Chelsea",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 145,
"host_name": "Scott",
"host_email": "scott@gmail.com"
},
{
"_id": 23,
"name": "Sydney Hyde Park City Apartment (checkin from 6am)",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 185,
"host_name": "Desireé",
"host_email": "desire@gmail.com"
},
{
"_id": 24,
"name": "THE Place to See Sydney's FIREWORKS",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 250,
"host_name": "Kristin",
"host_email": "kristin@gmail.com"
},
{
"_id": 25,
"name": "Downtown Oporto Inn (room cleaning)",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 40,
"host_name": "Elisabete",
"host_email": "elisabete@gmail.com"
},
{
"_id": 26,
"name": "GOLF ROYAL RESİDENCE TAXİM(1+1):3",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 838,
"host_name": "Ahmet",
"host_email": "ahmet@gmail.com"
},
{
"_id": 27,
"name": "GOLF ROYAL RESIDENCE SUİTES(2+1)-2",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 997,
"host_name": "Ahmet",
"host_email": "ahmet@gmail.com"
},
{
"_id": 28,
"name": "Apartamento zona sul do RJ",
"accommodates": 5,
"room_type": "Entire home/apt",
"pricePerNight": 933,
"host_name": "Luiz Rodrigo",
"host_email": "luiz@gmail.com"
},
{
"_id": 29,
"name": "A Casa Alegre é um apartamento T1.",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 40,
"host_name": "Manuela",
"host_email": "manuela@gmail.com"
},
{
"_id": 30,
"name": "The LES Apartment",
"accommodates": 3,
"room_type": "Entire home/apt",
"pricePerNight": 150,
"host_name": "Mert",
"host_email": "mert@gmail.com"
},
{
"_id": 31,
"name": "2 bedroom Upper east side",
"accommodates": 5,
"room_type": "Entire home/apt",
"pricePerNight": 275,
"host_name": "Chelsea",
"host_email": "chelsea@gmail.com"
},
{
"_id": 32,
"name": "Double and triple rooms Blue mosque",
"accommodates": 3,
"room_type": "Private room",
"pricePerNight": 121,
"host_name": "Mehmet Emin",
"host_email": "mehmet@gmail.com"
},
{
"_id": 33,
"name": "Room Close to LGA and 35 mins to Times Square",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 46,
"host_name": "Cheer",
"host_email": "cheer@gmail.com"
},
{
"_id": 34,
"name": "A bedroom far away from home",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 45,
"host_name": "Lane",
"host_email": "lane@gmail.com"
},
{
"_id": 35,
"name": "Big, Bright & Convenient Sheung Wan",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 966,
"host_name": "Regg",
"host_email": "regg@gmail.com"
},
{
"_id": 36,
"name": "Large railroad style 3 bedroom apt in Manhattan!",
"accommodates": 9,
"room_type": "Entire home/apt",
"pricePerNight": 180,
"host_name": "Vick",
"host_email": "vick@gmail.com"
},
{
"_id": 37,
"name": "Resort-like living in Williamsburg",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 220,
"host_name": "Mohammed",
"host_email": "mohammed@gmail.com"
},
{
"_id": 38,
"name": "Private Room (2) in Guest House at Coogee Beach",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 64,
"host_name": "David",
"host_email": "david@gmail.com"
},
{
"_id": 39,
"name": "Apto semi mobiliado",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 380,
"host_name": "Ricardo",
"host_email": "ricardo@gmail.com"
},
{
"_id": 40,
"name": "Roof double bed private room",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 185,
"host_name": "Mustafa",
"host_email": "mustafa@gmail.com"
},
{
"_id": 41,
"name": "Cozy Nest, heart of the Plateau",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 34,
"host_name": "Lilou",
"host_email": "lilou@gmail.com"
},
{
"_id": 42,
"name": "Uygun nezih daire",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 264,
"host_name": "Yaşar",
"host_email": "yaar@gmail.com"
},
{
"_id": 43,
"name": "Ipanema: moderno apê 2BR + garagem",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 298,
"host_name": "Barbara",
"host_email": "barbara@gmail.com"
},
{
"_id": 44,
"name": "Friendly Apartment, 10m from Manly",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 36,
"host_name": "Isaac",
"host_email": "isaac@gmail.com"
},
{
"_id": 45,
"name": "Great studio opp. Narrabeen Lake",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 117,
"host_name": "Tracy",
"host_email": "tracy@gmail.com"
},
{
"_id": 46,
"name": "Cozy Queen Guest Room&My",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 330,
"host_name": "Danny",
"host_email": "danny@gmail.com"
},
{
"_id": 47,
"name": "Cozy aptartment in Recreio (near Olympic Venues)",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 746,
"host_name": "José Augusto",
"host_email": "jos@gmail.com"
},
{
"_id": 48,
"name": "Kailua-Kona, Kona Coast II 2b condo",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 135,
"host_name": "Daniel",
"host_email": "daniel@gmail.com"
},
{
"_id": 49,
"name": "LAHAINA, MAUI! RESORT/CONDO BEACHFRONT!! SLEEPS 4!",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 499,
"host_name": "Holly",
"host_email": "holly@gmail.com"
},
{
"_id": 50,
"name": "Quarto inteiro na Tijuca",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 149,
"host_name": "Gilberto",
"host_email": "gilberto@gmail.com"
},
{
"_id": 51,
"name": "Twin Bed room+MTR Mongkok shopping&My",
"accommodates": 3,
"room_type": "Private room",
"pricePerNight": 400,
"host_name": "Danny",
"host_email": "danny@gmail.com"
},
{
"_id": 52,
"name": "Cozy double bed room 東涌鄉村雅緻雙人房",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 487,
"host_name": "Ricky",
"host_email": "ricky@gmail.com"
},
{
"_id": 53,
"name": "Your spot in Copacabana",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 798,
"host_name": "Ana Lúcia",
"host_email": "ana@gmail.com"
},
{
"_id": 54,
"name": "Makaha Valley Paradise with OceanView",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 95,
"host_name": "Ray And Lise",
"host_email": "ray@gmail.com"
},
{
"_id": 55,
"name": "IPANEMA LUXURY PENTHOUSE with MAID",
"accommodates": 3,
"room_type": "Entire home/apt",
"pricePerNight": 858,
"host_name": "Cesar",
"host_email": "cesar@gmail.com"
},
{
"_id": 56,
"name": "FloresRooms 3T",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 31,
"host_name": "Andreia",
"host_email": "andreia@gmail.com"
},
{
"_id": 57,
"name": "~Ao Lele~ Flying Cloud",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 185,
"host_name": "Mike",
"host_email": "mike@gmail.com"
},
{
"_id": 58,
"name": "Amazing and Big Apt, Ipanema Beach.",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 1999,
"host_name": "Pedro",
"host_email": "pedro@gmail.com"
},
{
"_id": 59,
"name": "Small Room w Bathroom Flamengo Rio de Janeiro",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 71,
"host_name": "Fernanda",
"host_email": "fernanda@gmail.com"
},
{
"_id": 60,
"name": "UWS Brownstone Near Central Park",
"accommodates": 3,
"room_type": "Entire home/apt",
"pricePerNight": 212,
"host_name": "Chas",
"host_email": "chas@gmail.com"
},
{
"_id": 61,
"name": "Alugo Apart frente mar Barra Tijuca",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 933,
"host_name": "Paulo Cesar",
"host_email": "paulo@gmail.com"
},
{
"_id": 62,
"name": "Cozy Art Top Floor Apt in PRIME Williamsburg!",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 175,
"host_name": "Ade",
"host_email": "ade@gmail.com"
},
{
"_id": 63,
"name": "Private OceanFront - Bathtub Beach. Spacious House",
"accommodates": 14,
"room_type": "Entire home/apt",
"pricePerNight": 795,
"host_name": "Noah",
"host_email": "noah@gmail.com"
},
{
"_id": 64,
"name": "Suíte em local tranquilo e seguro",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 101,
"host_name": "Renato",
"host_email": "renato@gmail.com"
},
{
"_id": 65,
"name": "A large sunny bedroom",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 35,
"host_name": "Ehssan",
"host_email": "ehssan@gmail.com"
},
{
"_id": 66,
"name": "Best location 1BR Apt in HK - Shops & Sights",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 997,
"host_name": "Simon",
"host_email": "simon@gmail.com"
},
{
"_id": 67,
"name": "Room For Erasmus",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 37,
"host_name": "Kemal",
"host_email": "kemal@gmail.com"
},
{
"_id": 68,
"name": "",
"accommodates": 4,
"room_type": "Private room",
"pricePerNight": 105,
"host_name": "Seda",
"host_email": "seda@gmail.com"
},
{
"_id": 69,
"name": "BBC OPORTO 4X2",
"accommodates": 8,
"room_type": "Entire home/apt",
"pricePerNight": 100,
"host_name": "Cristina",
"host_email": "cristina@gmail.com"
},
{
"_id": 70,
"name": "Apartamento Mobiliado - Lgo do Machado",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 149,
"host_name": "Marcelo",
"host_email": "marcelo@gmail.com"
},
{
"_id": 71,
"name": "luxury apartment in istanbul taxsim",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 269,
"host_name": "Osman",
"host_email": "osman@gmail.com"
},
{
"_id": 72,
"name": "Pousada das Colonias",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 138,
"host_name": "Lidia Maria",
"host_email": "lidia@gmail.com"
},
{
"_id": 73,
"name": "Quarto Taquara - Jacarepaguá",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 101,
"host_name": "Luana",
"host_email": "luana@gmail.com"
},
{
"_id": 74,
"name": "Banyan Bungalow",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 100,
"host_name": "Bobby",
"host_email": "bobby@gmail.com"
},
{
"_id": 75,
"name": "Beautiful flat with services",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 351,
"host_name": "Liliane",
"host_email": "liliane@gmail.com"
},
{
"_id": 76,
"name": "(1) Beach Guest House - Go Make A Trip",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 112,
"host_name": "Rafael",
"host_email": "rafael@gmail.com"
},
{
"_id": 77,
"name": "Studio convenient to CBD, beaches, street parking.",
"accommodates": 5,
"room_type": "Entire home/apt",
"pricePerNight": 45,
"host_name": "Leslie",
"host_email": "leslie@gmail.com"
},
{
"_id": 78,
"name": "Bondi Beach Dreaming 3-Bed House",
"accommodates": 8,
"room_type": "Entire home/apt",
"pricePerNight": 399,
"host_name": "Cat",
"host_email": "cat@gmail.com"
},
{
"_id": 79,
"name": "March 2019 availability! Oceanview on Sugar Beach!",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 229,
"host_name": "Gage",
"host_email": "gage@gmail.com"
},
{
"_id": 80,
"name": "Sala e quarto em copacabana com cozinha americana",
"accommodates": 3,
"room_type": "Entire home/apt",
"pricePerNight": 298,
"host_name": "Tamara",
"host_email": "tamara@gmail.com"
},
{
"_id": 81,
"name": "Where Castles and Art meet the Sea",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 80,
"host_name": "Jose",
"host_email": "jose@gmail.com"
},
{
"_id": 82,
"name": "Aluguel Temporada Casa São Conrado",
"accommodates": 11,
"room_type": "Entire home/apt",
"pricePerNight": 2499,
"host_name": "Maria Pia",
"host_email": "maria@gmail.com"
},
{
"_id": 83,
"name": "Homely Room in 5-Star New Condo@MTR",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 479,
"host_name": "Crystal",
"host_email": "crystal@gmail.com"
},
{
"_id": 84,
"name": "Greenwich Fun and Luxury",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 999,
"host_name": "Craig",
"host_email": "craig@gmail.com"
},
{
"_id": 85,
"name": "The Garden Studio",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 129,
"host_name": "Cath",
"host_email": "cath@gmail.com"
},
{
"_id": 86,
"name": "Rented Room",
"accommodates": 1,
"room_type": "Private room",
"pricePerNight": 112,
"host_name": "Jercilene",
"host_email": "jercilene@gmail.com"
},
{
"_id": 87,
"name": "Cheerful new renovated central apt",
"accommodates": 8,
"room_type": "Entire home/apt",
"pricePerNight": 264,
"host_name": "Aybike",
"host_email": "aybike@gmail.com"
},
{
"_id": 88,
"name": "Heroísmo IV",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 29,
"host_name": "Apartments2Enjoy",
"host_email": "apartments2enjoy@gmail.com"
},
{
"_id": 89,
"name": "Cozy House in Ortaköy",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 100,
"host_name": "Orcun",
"host_email": "orcun@gmail.com"
},
{
"_id": 90,
"name": "Condomínio Praia Barra da Tijuca",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 351,
"host_name": "Paula",
"host_email": "paula@gmail.com"
},
{
"_id": 91,
"name": "位於深水埗地鐵站的溫馨公寓",
"accommodates": 4,
"room_type": "Private room",
"pricePerNight": 353,
"host_name": "Aaron",
"host_email": "aaron@gmail.com"
},
{
"_id": 92,
"name": "Tropical Jungle Oasis",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 125,
"host_name": "Douglas",
"host_email": "douglas@gmail.com"
},
{
"_id": 93,
"name": "Luxury 1-Bdrm in Downtown Brooklyn",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 144,
"host_name": "Ashley",
"host_email": "ashley@gmail.com"
},
{
"_id": 94,
"name": "Cozy bedroom Sagrada Familia",
"accommodates": 2,
"room_type": "Private room",
"pricePerNight": 20,
"host_name": "Rapha",
"host_email": "rapha@gmail.com"
},
{
"_id": 95,
"name": "Whole Apt. in East Williamsburg",
"accommodates": 4,
"room_type": "Entire home/apt",
"pricePerNight": 125,
"host_name": "Guillermo",
"host_email": "guillermo@gmail.com"
},
{
"_id": 96,
"name": "Jubilee By The Sea (Ocean Views)",
"accommodates": 11,
"room_type": "Entire home/apt",
"pricePerNight": 325,
"host_name": "Nate",
"host_email": "nate@gmail.com"
},
{
"_id": 97,
"name": "Sun filled comfortable apartment",
"accommodates": 2,
"room_type": "Entire home/apt",
"pricePerNight": 85,
"host_name": "Harry",
"host_email": "harry@gmail.com"
},
{
"_id": 98,
"name": "Park Guell apartment with terrace",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 85,
"host_name": "Alexandra Y Juan",
"host_email": "alexandra@gmail.com"
},
{
"_id": 99,
"name": "Spacious and well located apartment",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 60,
"host_name": "Andre",
"host_email": "andre@gmail.com"
},
{
"_id": 100,
"name": "Jardim Botânico Gourmet 2 bdroom",
"accommodates": 6,
"room_type": "Entire home/apt",
"pricePerNight": 395,
"host_name": "Roberta (Beta) Gatti",
"host_email": "roberta@gmail.com"
}
]
Search Index(name: default)
{
"mappings": {
"fields": { // name, room_type field에 정적 매핑 인덱스 생성
"name": [
{ "type": "string" }
],
"room_type": [
{ "type": "string" }
]
}
}
}
- Query
[
{
$search: {
index: "default", // default 이름을 가진 search index 사용
text: {
query: "small room apt",
path: ["room_type", "name"] // room_type, name 필드에 검색
}
},
},
{
"$limit": 5 // 결과 5개만 반환
},
{
"$project": {
"name": 1,
"room_type": 1,
// searchSequenceToken 반환
"paginationToken" : { "$meta" : "searchSequenceToken" },
// 문서 일치 score 반환
"score": { "$meta": "searchScore" }
}
}
]
- Result
- 검색어 정확도 순으로
paginationToken을 포함한 10개의 결과 반환
- 검색어 정확도 순으로
[
{
"_id": 59,
"name": "Small Room w Bathroom Flamengo Rio de Janeiro",
"room_type": "Private room",
"paginationToken": "CDoVdCYqQCICEHY=", // searchSequenceToken
"score": 2.658596992492676 // 정확도 점수 순으로
},
{
"_id": 2,
"name": "Horto flat with small garden",
"room_type": "Entire home/apt",
"paginationToken": "CAEVgtb0PyICEAQ=",
"score": 1.9127962589263916
},
{
"_id": 5,
"name": "Apt Linda Vista Lagoa - Rio",
"room_type": "Private room",
"paginationToken": "CAQVr0DcPyICEAo=",
"score": 1.7207239866256714
},
{
"_id": 86,
"name": "Rented Room",
"room_type": "Private room",
"paginationToken": "CFUVpibVPyIDEKwB",
"score": 1.6652419567108154
},
{
"_id": 6,
"name": "New York City - Upper West Side Apt",
"room_type": "Private room",
"paginationToken": "CAUVwE3IPyICEAw=",
"score": 1.5648727416992188
}
]
Ref.
https://www.mongodb.com/ko-kr/docs/atlas/atlas-search/atlas-search-overview/
https://mongodb-developer.github.io/search-lab/docs/search/search-index
https://tech.inflab.com/202211-mongodb-atlas-search/
'DEV > DB' 카테고리의 다른 글
| 3분만에 알아보는 Redis를 사용한 분산 락 (0) | 2025.10.05 |
|---|