Docs Menu
Docs Home
/
MongoDB Atlas
/ /

Atlas Vector Search를 사용하여 로컬 RAG 구현 구축

이 페이지의 내용

  • 배경
  • 전제 조건
  • 로컬 배포 또는 Atlas 클러스터 만들기
  • 환경 설정
  • 로컬 모델로 임베딩 생성
  • Atlas Vector Search 인덱스 만들기
  • 로컬 LLM으로 질문에 답변하기

이 튜토리얼은 API 키나 크레딧 없이 로컬에서 검색 증강 생성(RAG)을 구현하는 방법을 보여줍니다. RAG에 대해 자세히 알아보려면 Atlas Vector Search를 사용한 검색 증강 생성(RAG)을 참조하세요.

구체적으로 다음 조치를 수행합니다.

  1. 로컬 Atlas 배포서버 서버를 만들거나 cloud 에 클러스터 를 배포 하세요.

  2. 환경을 설정합니다.

  3. 로컬 임베딩 모델을 사용하여 벡터 임베딩을 생성합니다.

  4. 데이터에 Atlas Vector Search 검색 인덱스를 만듭니다.

  5. 로컬 LLM을 사용하여 데이터 관련 질문에 답합니다.


언어 선택 드롭다운 메뉴를 사용하여 이 페이지에 있는 예시의 언어를 설정합니다.


이 튜토리얼을 완료하려면 Atlas CLI를 사용하여 로컬 Atlas 배포를 생성하거나 클라우드에 클러스터를 배포할 수 있습니다. Atlas CLI는 MongoDB Atlas의 명령줄 인터페이스이며, Atlas CLI를 사용하여 터미널에서 로컬 Atlas 배포를 생성하는 등 다양한 작업을 위해 Atlas와 상호 작용할 수 있습니다. 자세한 내용은 Atlas CLI에서 로컬 및 Cloud 배포 관리를 참조하세요.

참고

로컬 Atlas 배포는 테스트 목적으로만 사용할 수 있습니다. 프로덕션 환경의 경우에는 클러스터를 배포하세요.

이 튜토리얼에서는 다음과 같은 오픈 소스 모델도 사용합니다.

LLM을 로컬로 다운로드하고 배포하는 방법에는 여러 가지가 있습니다. 이 튜토리얼에서는 Ollama를 다운로드하고 위에 나열된 오픈소스 모델을 가져와서 RAG 작업을 수행합니다.

또한 이 튜토리얼에서는 인기 있는 오픈 소스 LLM 프레임워크인 LangChain의 Go 언어 포트를 사용하여 이러한 모델에 연결하고 Atlas Vector Search와 통합합니다. 다른 모델이나 다른 프레임워크를 선호하는 경우 Ollama 모델 이름과 LangChain 라이브러리 구성 요소를 원하는 설정을 위한 모델 및 구성 요소로 대체하여 이 튜토리얼을 조정할 수 있습니다.

LLM을 로컬에다운로드 하고 배포 방법에는 여러 가지가 있습니다. 이 튜토리얼에서는 Ollama를 다운로드 하고 다음 오픈 소스 모델을 가져와서 RAG 작업을 수행합니다.

또한 이 튜토리얼에서는 널리 사용되는 Java 용 오픈 소스 LLM 프레임워크 인 LangChain4j를 사용하여 이러한 모델에 연결하고 Atlas Vector Search 와 통합합니다. 다른 모델이나 다른 프레임워크 를 선호하는 경우 Ollama 모델 이름 또는 LangChain4j 라이브러리 구성 요소를 원하는 설정 에 해당하는 것으로 대체하여 이 튜토리얼을 조정할 수 있습니다.

이 튜토리얼에서는 다음과 같은 오픈 소스 모델도 사용합니다.

LLM을 로컬로 다운로드하고 배포하는 방법에는 여러 가지가 있습니다. 이 튜토리얼에서는 로컬 LLM개발을 위한 오픈 소스 에코시스템인 GPT4All을 사용하여 Mistral 7B 모델을 다운로드합니다.

이 튜토리얼을 진행할 때는 대화형 Python 노트북을 사용하세요. 이 환경에서는 매번 전체 파일을 실행하지 않고도 개별 코드 블록을 만들고 실행할 수 있습니다.

이 튜토리얼에서는 다음과 같은 오픈 소스 모델도 사용합니다.

LLM을 로컬로 다운로드하고 배포하는 방법에는 여러 가지가 있습니다. 이 튜토리얼에서는 로컬 LLM개발을 위한 오픈 소스 에코시스템인 GPT4All을 사용하여 Mistral 7B 모델을 다운로드합니다.

이 튜토리얼을 완료하려면 다음 조건을 충족해야 합니다.

이 튜토리얼을 완료하려면 다음 조건을 충족해야 합니다.

  • Java Development Kit (JDK) 버전 8 이상.

  • Java 애플리케이션 을 설정하다 하고 실행 하기 위한 환경입니다.IntelliJ IDEA 또는 Eclipse IDE와 같은 통합 개발 환경(IDE)을 사용하여 프로젝트 를 빌드 하고 실행 하도록 Maven 또는 Gradle을 구성하는 것이 좋습니다.

  • Ollama가 설치되었습니다.

이 튜토리얼을 완료하려면 다음 조건을 충족해야 합니다.

이 튜토리얼을 완료하려면 다음 조건을 충족해야 합니다.

  • Atlas CLI가 설치되어 v1.14.3 이상을 실행 중입니다.

  • MongoDB 명령줄 데이터베이스 도구 가 설치되었습니다.

  • 로컬에서 실행할 수 있는 대화형 Python 노트북입니다. VS Code에서 대화형 Python 노트북을 실행할 수 있습니다. 사용자 환경에서 Python v3.10 또는 이후 버전을 실행 중인지 확인하세요.

참고

Colab과 같은 호스팅 서비스를 사용하는 경우, 이번 튜토리얼을 실행하기에 RAM이 충분한지 확인하세요. RAM이 부족한 경우 성능 문제가 발생할 수 있습니다.

이 튜토리얼을 사용하려면 벡터 데이터베이스 로 사용할 샘플 AirBnB 목록 데이터 세트가 로드된 로컬 또는 cloud Atlas 배포서버 서버가 필요합니다.

sample_airbnb.listingsAndReviews 샘플 데이터가 로드된 상태에서 MongoDB 버전 6.0.11 7.0.2 이상을 실행 하는 기존 Atlas cluster 가 있는 경우 이 단계를 건너뛸 수 있습니다.

Atlas CLI 를 사용하여 로컬 Atlas 배포서버 서버를 만들거나 cloud 에 클러스터 를 배포 수 있습니다.

Atlas CLI 를 사용하여 로컬 배포서버 를 만들 수 있습니다.

1

터미널에서 atlas auth login을 실행하여 Atlas 로그인 자격 증명을 인증합니다. 자세한 내용은 Atlas CLI에서 연결을 참조하세요.

참고

기존 Atlas 계정이 없다면 atlas setup을 실행하거나 새 계정을 생성하세요.

2

atlas deployments setup을 실행하고 메시지에 따라 로컬 배포를 만듭니다.

자세한 지침은 로컬 Atlas 배포 생성을 참조하세요.

3
  1. 터미널에서 다음 명령을 실행하여 샘플 데이터를 다운로드합니다.

    curl https://atlas-education.s3.amazonaws.com/sampledata.archive -o sampledata.archive
  2. 다음 명령을 실행하여 데이터를 배포서버에 로드하고 <port-number>를 배포서버를 호스팅하는 포트로 바꿉니다.

    mongorestore --archive=sampledata.archive --port=<port-number>

    참고

    명령에 액세스 하려면 MongoDB 명령줄 데이터베이스 도구 를 설치해야 합니다.mongorestore

Atlas CLI 또는 Atlas UI 를 사용하여 새 클러스터 를 만들고 배포 수 있습니다.샘플 데이터로 새 클러스터 를 미리 로드해야 합니다.

Atlas에서 제공하는 샘플 데이터를 클러스터에 로드하는 방법을 알아보려면 샘플 데이터 로드를 참조하세요.

자세한 지침은 클러스터 생성을 참조하세요.

이 섹션에서는 이 자습서를 위한 환경을 설정합니다. 프로젝트를 생성하고, 필요한 패키지를 설치하고, 연결 문자열 을 정의합니다.

1

터미널에서 다음 명령을 실행하여 local-rag-mongodb라는 새 디렉터리를 만들고 프로젝트를 초기화합니다.

mkdir local-rag-mongodb
cd local-rag-mongodb
go mod init local-rag-mongodb
2

다음 명령을 실행합니다.

go get github.com/joho/godotenv
go get go.mongodb.org/mongo-driver/mongo
go get github.com/tmc/langchaingo/llms
go get github.com/tmc/langchaingo/llms/ollama
go get github.com/tmc/langchaingo/prompts
3

프로젝트 에서 연결 문자열을 저장할 .env 파일을 만듭니다.

.env
ATLAS_CONNECTION_STRING = "<connection-string>"

<connection-string> 자리 표시자 값을 Atlas 연결 문자열로 바꿉니다.

로컬 Atlas 배포를 사용하는 경우 연결 문자열은 이 형식을 따르며, <port-number>를 로컬 배포용 포트와 바꿉니다.

ATLAS_CONNECTION_STRING = "mongodb://localhost:<port-number>/?directConnection=true"

Atlas 클러스터를 사용 중이라면 연결 문자열은 "<connection-string>";을 Atlas 클러스터의 SRV 연결 문자열로 대체하여 다음 형식을 따릅니다.

ATLAS_CONNECTION_STRING = "<connection-string>"

참고

연결 문자열은 다음 형식을 사용해야 합니다.

mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net

이 섹션에서는 이 자습서를 위한 환경을 설정합니다. 프로젝트를 생성하고, 필요한 패키지를 설치하고, 연결 문자열 을 정의합니다.

1
  1. IDE에서 Maven 또는 Gradle을 사용하여 local-rag-mongodb 이라는 이름의 Java 프로젝트 를 만듭니다.

  2. 패키지 관리자에 따라 다음 종속성을 추가합니다.

    Maven을 사용하는 경우 프로젝트의 pom.xml 파일 에 있는 dependencies 배열 에 다음 종속성을 추가합니다.

    pom.xml
    <dependencies>
    <!-- MongoDB Java Sync Driver v5.2.0 or later -->
    <dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongodb-driver-sync</artifactId>
    <version>[5.2.0,)</version>
    </dependency>
    <!-- Java library for working with Ollama -->
    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ollama</artifactId>
    <version>0.35.0</version>
    </dependency>
    </dependencies>

    Gradle을 사용하는 경우 프로젝트의 build.gradle 파일 에 있는 dependencies 배열 에 다음을 추가합니다.

    build.gradle
    dependencies {
    // MongoDB Java Sync Driver v5.2.0 or later
    implementation 'org.mongodb:mongodb-driver-sync:[5.2.0,)'
    // Java library for working with Ollama
    implementation 'dev.langchain4j:langchain4j-ollama:0.35.0'
    }
  3. 패키지 관리자를 실행하여 프로젝트 에 종속성을 설치합니다.

2

참고

이 예시 에서는 IDE에서 변수를 설정합니다. 프로덕션 애플리케이션은 배포서버 구성, CI/CD 파이프라인 또는 시크릿 관리자를 통해 환경 변수를 관리 할 수 있지만, 제공된 코드를 사용 사례 에 맞게 조정할 수 있습니다.

IDE에서 새 구성 템플릿을 만들고 프로젝트 에 다음 변수를 추가합니다.

  • IntelliJ IDEA를 사용하는 경우 새 Application 실행 구성 템플릿을 만든 다음 Environment variables 필드 에 변수를 세미콜론으로 구분된 값으로 추가합니다( 예시: FOO=123;BAR=456). 변경 사항을 적용하고 OK를 클릭합니다.

    학습 내용은 IntelliJ IDEA 문서의 템플릿에서 실행/디버그 구성 만들기 섹션을 참조하세요.

  • Eclipse를 사용하는 경우 새 Java Application 시작 구성을 만든 다음 Environment 탭 에서 각 변수를 새 키-값 쌍으로 추가합니다. 변경 사항을 적용하고 OK를 클릭합니다.

    학습 내용은 Eclipse IDE 문서의 Java 애플리케이션 실행 구성 생성하기 섹션을 참조하세요.

<port-number> 을 로컬 배포서버 포트로 바꿉니다.

연결 문자열 은 다음 형식을 따라야 합니다.

ATLAS_CONNECTION_STRING = "mongodb://localhost:<port-number>/?directConnection=true"

<connection-string> 자리 표시자 값을 Atlas 클러스터의 SRV 연결 문자열로 바꿉니다.

연결 문자열은 다음 형식을 사용해야 합니다.

mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net

이 섹션에서는 이 자습서를 위한 환경을 설정합니다. 프로젝트를 생성하고, 필요한 패키지를 설치하고, 연결 문자열 을 정의합니다.

1

터미널에서 다음 명령을 실행하여 local-rag-mongodb라는 새 디렉터리를 만들고 프로젝트를 초기화합니다.

mkdir local-rag-mongodb
cd local-rag-mongodb
npm init -y
2

다음 명령을 실행합니다:

npm install mongodb @xenova/transformers node-gyp gpt4all
3

다음 예시와 같이 프로젝트의 package.json 파일에서 type 필드를 지정한 다음 파일을 저장합니다.

package.json
{
"name": "local-rag-mongodb",
"type": "module",
...
}
4

프로젝트 에서 연결 문자열을 저장할 .env 파일을 만듭니다.

.env
ATLAS_CONNECTION_STRING = "<connection-string>"

<connection-string> 자리 표시자 값을 Atlas 연결 문자열로 바꿉니다.

로컬 Atlas 배포를 사용하는 경우 연결 문자열은 이 형식을 따르며, <port-number>를 로컬 배포용 포트와 바꿉니다.

ATLAS_CONNECTION_STRING = "mongodb://localhost:<port-number>/?directConnection=true";

Atlas 클러스터를 사용 중이라면 연결 문자열은 "<connection-string>";을 Atlas 클러스터의 SRV 연결 문자열로 대체하여 다음 형식을 따릅니다.

ATLAS_CONNECTION_STRING = "<connection-string>";

참고

연결 문자열은 다음 형식을 사용해야 합니다.

mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net

참고

최소 Node.js 버전 요구 사항

Node.js v20.x --env-file 옵션을 도입했습니다. 이전 버전의 Node.js를 사용하는 경우 프로젝트에 dotenv 패키지를 추가하거나 다른 방법으로 환경 변수를 관리하세요.

이 섹션에서는 이 자습서를 위한 환경을 설정합니다.

1

터미널에서 다음 명령을 실행하여 local-rag-mongodb 이라는 새 디렉토리를 만듭니다.

mkdir local-rag-mongodb
cd local-rag-mongodb
2

local-rag-mongodb 디렉토리에 .ipynb 확장자를 가진 파일 을 저장합니다. 이 튜토리얼의 나머지 코드 스니펫을 노트북에서 실행합니다. 각 스니펫에 대해 새 코드 블록을 만들어야 합니다.

3

노트북에서 다음 명령을 실행합니다.

pip install --quiet pymongo gpt4all sentence_transformers
4

로컬 Atlas 배포를 사용하는 경우 노트북에서 다음 코드를 실행하여 <port-number>를 로컬 배포의 포트로 변경합니다.

ATLAS_CONNECTION_STRING = ("mongodb://localhost:<port-number>/?directConnection=true")

Atlas 클러스터를 사용하는 경우 노트북에서 다음 코드를 실행하고 <connection-string>를 Atlas 클러스터의 SRV 연결 문자열로 바꿉니다.

ATLAS_CONNECTION_STRING = ("<connection-string>")

참고

연결 문자열은 다음 형식을 사용해야 합니다.

mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net

이 섹션에서는 임베딩 모델을 로컬에 로드하고 listingsAndReviews라는 단일 컬렉션이 포함된 sample_airbnb 데이터베이스의 데이터를 사용하여 벡터 임베딩을 만듭니다.

1

이 예시에서는 Ollama의 nomic-embed-text 모델을 사용합니다.

다음 명령을 실행하여 임베딩 모델을 가져옵니다.

ollama pull nomic-embed-text
2
  1. 여러 단계에서 재사용할 코드를 저장할 common 디렉토리를 만듭니다.

    mkdir common && cd common
  2. get-embeddings.go 파일을 생성하고 다음 코드를 붙여넣습니다.

    get-embeddings.go
    package common
    import (
    "context"
    "log"
    "github.com/tmc/langchaingo/llms/ollama"
    )
    func GetEmbeddings(documents []string) [][]float32 {
    llm, err := ollama.New(ollama.WithModel("nomic-embed-text"))
    if err != nil {
    log.Fatalf("failed to connect to ollama: %v", err)
    }
    ctx := context.Background()
    embs, err := llm.CreateEmbedding(ctx, documents)
    if err != nil {
    log.Fatalf("failed to create ollama embedding: %v", err)
    }
    return embs
    }
  3. 이 컬렉션에 있는 문서를 BSON으로 또는 BSON에서 마샬링하고 마샬링을 해제하는 작업을 단순화하려면 models.go 파일을 만들고 이 파일에 다음 코드를 붙여넣으세요.

    models.go
    package common
    import (
    "time"
    "go.mongodb.org/mongo-driver/bson/primitive"
    )
    type Image struct {
    ThumbnailURL string `bson:"thumbnail_url"`
    MediumURL string `bson:"medium_url"`
    PictureURL string `bson:"picture_url"`
    XLPictureURL string `bson:"xl_picture_url"`
    }
    type Host struct {
    ID string `bson:"host_id"`
    URL string `bson:"host_url"`
    Name string `bson:"host_name"`
    Location string `bson:"host_location"`
    About string `bson:"host_about"`
    ThumbnailURL string `bson:"host_thumbnail_url"`
    PictureURL string `bson:"host_picture_url"`
    Neighborhood string `bson:"host_neighborhood"`
    IsSuperhost bool `bson:"host_is_superhost"`
    HasProfilePic bool `bson:"host_has_profile_pic"`
    IdentityVerified bool `bson:"host_identity_verified"`
    ListingsCount int32 `bson:"host_listings_count"`
    TotalListingsCount int32 `bson:"host_total_listings_count"`
    Verifications []string `bson:"host_verifications"`
    }
    type Location struct {
    Type string `bson:"type"`
    Coordinates []float64 `bson:"coordinates"`
    IsLocationExact bool `bson:"is_location_exact"`
    }
    type Address struct {
    Street string `bson:"street"`
    Suburb string `bson:"suburb"`
    GovernmentArea string `bson:"government_area"`
    Market string `bson:"market"`
    Country string `bson:"Country"`
    CountryCode string `bson:"country_code"`
    Location Location `bson:"location"`
    }
    type Availability struct {
    Thirty int32 `bson:"availability_30"`
    Sixty int32 `bson:"availability_60"`
    Ninety int32 `bson:"availability_90"`
    ThreeSixtyFive int32 `bson:"availability_365"`
    }
    type ReviewScores struct {
    Accuracy int32 `bson:"review_scores_accuracy"`
    Cleanliness int32 `bson:"review_scores_cleanliness"`
    CheckIn int32 `bson:"review_scores_checkin"`
    Communication int32 `bson:"review_scores_communication"`
    Location int32 `bson:"review_scores_location"`
    Value int32 `bson:"review_scores_value"`
    Rating int32 `bson:"review_scores_rating"`
    }
    type Review struct {
    ID string `bson:"_id"`
    Date time.Time `bson:"date,omitempty"`
    ListingId string `bson:"listing_id"`
    ReviewerId string `bson:"reviewer_id"`
    ReviewerName string `bson:"reviewer_name"`
    Comments string `bson:"comments"`
    }
    type Listing struct {
    ID string `bson:"_id"`
    ListingURL string `bson:"listing_url"`
    Name string `bson:"name"`
    Summary string `bson:"summary"`
    Space string `bson:"space"`
    Description string `bson:"description"`
    NeighborhoodOverview string `bson:"neighborhood_overview"`
    Notes string `bson:"notes"`
    Transit string `bson:"transit"`
    Access string `bson:"access"`
    Interaction string `bson:"interaction"`
    HouseRules string `bson:"house_rules"`
    PropertyType string `bson:"property_type"`
    RoomType string `bson:"room_type"`
    BedType string `bson:"bed_type"`
    MinimumNights string `bson:"minimum_nights"`
    MaximumNights string `bson:"maximum_nights"`
    CancellationPolicy string `bson:"cancellation_policy"`
    LastScraped time.Time `bson:"last_scraped,omitempty"`
    CalendarLastScraped time.Time `bson:"calendar_last_scraped,omitempty"`
    FirstReview time.Time `bson:"first_review,omitempty"`
    LastReview time.Time `bson:"last_review,omitempty"`
    Accommodates int32 `bson:"accommodates"`
    Bedrooms int32 `bson:"bedrooms"`
    Beds int32 `bson:"beds"`
    NumberOfReviews int32 `bson:"number_of_reviews"`
    Bathrooms primitive.Decimal128 `bson:"bathrooms"`
    Amenities []string `bson:"amenities"`
    Price primitive.Decimal128 `bson:"price"`
    WeeklyPrice primitive.Decimal128 `bson:"weekly_price"`
    MonthlyPrice primitive.Decimal128 `bson:"monthly_price"`
    CleaningFee primitive.Decimal128 `bson:"cleaning_fee"`
    ExtraPeople primitive.Decimal128 `bson:"extra_people"`
    GuestsIncluded primitive.Decimal128 `bson:"guests_included"`
    Image Image `bson:"images"`
    Host Host `bson:"host"`
    Address Address `bson:"address"`
    Availability Availability `bson:"availability"`
    ReviewScores ReviewScores `bson:"review_scores"`
    Reviews []Review `bson:"reviews"`
    Embeddings []float32 `bson:"embeddings,omitempty"`
    }
  4. 루트 디렉토리로 돌아갑니다.

    cd ../
  5. generate-embeddings.go라는 다른 파일을 만들고 그 안에 다음 코드를 붙여넣습니다.

    generate-embeddings.go
    1package main
    2
    3import (
    4 "context"
    5 "local-rag-mongodb/common" // Module that contains the models and GetEmbeddings function
    6 "log"
    7 "os"
    8
    9 "github.com/joho/godotenv"
    10 "go.mongodb.org/mongo-driver/bson"
    11 "go.mongodb.org/mongo-driver/mongo"
    12 "go.mongodb.org/mongo-driver/mongo/options"
    13)
    14
    15func main() {
    16 ctx := context.Background()
    17
    18 if err := godotenv.Load(); err != nil {
    19 log.Println("no .env file found")
    20 }
    21
    22 // Connect to your Atlas cluster
    23 uri := os.Getenv("ATLAS_CONNECTION_STRING")
    24 if uri == "" {
    25 log.Fatal("set your 'ATLAS_CONNECTION_STRING' environment variable.")
    26 }
    27 clientOptions := options.Client().ApplyURI(uri)
    28 client, err := mongo.Connect(ctx, clientOptions)
    29 if err != nil {
    30 log.Fatalf("failed to connect to the server: %v", err)
    31 }
    32 defer func() { _ = client.Disconnect(ctx) }()
    33
    34 // Set the namespace
    35 coll := client.Database("sample_airbnb").Collection("listingsAndReviews")
    36
    37 filter := bson.D{
    38 {"$and",
    39 bson.A{
    40 bson.D{
    41 {"$and",
    42 bson.A{
    43 bson.D{{"summary", bson.D{{"$exists", true}}}},
    44 bson.D{{"summary", bson.D{{"$ne", ""}}}},
    45 },
    46 }},
    47 bson.D{{"embeddings", bson.D{{"$exists", false}}}},
    48 }},
    49 }
    50
    51 findOptions := options.Find().SetLimit(250)
    52
    53 cursor, err := coll.Find(ctx, filter, findOptions)
    54 if err != nil {
    55 log.Fatalf("failed to retrieve data from the server: %v", err)
    56 }
    57
    58 var listings []common.Listing
    59 if err = cursor.All(ctx, &listings); err != nil {
    60 log.Fatalf("failed to unmarshal retrieved docs to model objects: %v", err)
    61 }
    62
    63 var summaries []string
    64 for _, listing := range listings {
    65 summaries = append(summaries, listing.Summary)
    66 }
    67
    68 log.Println("Generating embeddings.")
    69 embeddings := common.GetEmbeddings(summaries)
    70
    71 updateDocuments := make([]mongo.WriteModel, len(listings))
    72 for i := range updateDocuments {
    73 updateDocuments[i] = mongo.NewUpdateOneModel().
    74 SetFilter(bson.D{{"_id", listings[i].ID}}).
    75 SetUpdate(bson.D{{"$set", bson.D{{"embeddings", embeddings[i]}}}})
    76 }
    77
    78 bulkWriteOptions := options.BulkWrite().SetOrdered(false)
    79
    80 result, err := coll.BulkWrite(ctx, updateDocuments, bulkWriteOptions)
    81 if err != nil {
    82 log.Fatalf("failed to update documents: %v", err)
    83 }
    84
    85 log.Printf("%d documents updated successfully.", result.MatchedCount)
    86}

    이 예사에서는 임베딩을 생성할 때 문서 갯수를 250개로 제한했습니다. 컬렉션에 있는 5000개 이상의 문서에 대해 임베딩을 생성하면 프로세스가 느리게 처리됩니다. 임베딩을 생성하는 문서 수를 변경하려는 경우:

    • 문서 수 변경: 52행에 있는 Find() 옵션에서 .SetLimit(250) 숫자를 조정합니다.

    • 모든 문서에 대해 임베딩 생성: 54행의 Find() 호출에서 옵션을 생략합니다.

  6. 다음 명령을 실행하여 코드를 실행합니다.

    go run generate-embeddings.go
    2024/10/10 15:49:23 Generating embeddings.
    2024/10/10 15:49:28 250 documents updated successfully.
1

다음 명령을 실행하여 Ollama에서 nomic-embed-text 모델을 가져옵니다.

ollama pull nomic-embed-text
2

OllamaModels.java 라는 파일 을 만들고 다음 코드를 붙여넣습니다.

이 코드는 프로젝트 에서 사용할 로컬 Ollama 임베딩 및 채팅 모델을 정의합니다. 이후 단계에서 채팅 모델로 작업하겠습니다. 원하는 설정 에 맞게 추가 모델을 조정하거나 만들 수 있습니다.

또한 이 코드는 이전에 다운로드한 임베딩 모델을 사용하여 지정된 입력에 대한 임베딩을 생성하는 두 가지 메서드를 정의합니다.

  • 다중 입력 :getEmbeddings 메서드는 텍스트 입력 배열 (List<String>)을 허용하므로 단일 API 호출로 여러 임베딩을 생성할 수 있습니다. 이 메서드는 Atlas cluster 에 저장하기 위해 API 에서 제공하는 float 배열을 double 의 BSON 배열로 변환합니다.

  • 단일 입력: 메서드는 getEmbedding String벡터 데이터에 대해 수행하려는 쿼리 를 나타내는 단일 을 허용합니다. 이 메서드는 컬렉션 을 쿼리할 때 사용할 수 있도록 API 에서 제공하는 부동 소수점 배열 을 double의 BSON 배열 로 변환합니다.

OllamaModels.java
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.model.output.Response;
import org.bson.BsonArray;
import org.bson.BsonDouble;
import java.util.List;
import static java.time.Duration.ofSeconds;
public class OllamaModels {
private static final String host = "http://localhost:11434";
private static OllamaEmbeddingModel embeddingModel;
private static OllamaChatModel chatModel;
/**
* Returns the Ollama embedding model used by the getEmbeddings() and getEmbedding() methods
* to generate vector embeddings.
*/
public static OllamaEmbeddingModel getEmbeddingModel() {
if (embeddingModel == null) {
embeddingModel = OllamaEmbeddingModel.builder()
.timeout(ofSeconds(10))
.modelName("nomic-embed-text")
.baseUrl(host)
.build();
}
return embeddingModel;
}
/**
* Returns the Ollama chat model interface used by the createPrompt() method
* to process queries and generate responses.
*/
public static OllamaChatModel getChatModel() {
if (chatModel == null) {
chatModel = OllamaChatModel.builder()
.timeout(ofSeconds(25))
.modelName("mistral")
.baseUrl(host)
.build();
}
return chatModel;
}
/**
* Takes an array of strings and returns a collection of BSON array embeddings
* to store in the database.
*/
public static List<BsonArray> getEmbeddings(List<String> texts) {
List<TextSegment> textSegments = texts.stream()
.map(TextSegment::from)
.toList();
Response<List<Embedding>> response = getEmbeddingModel().embedAll(textSegments);
return response.content().stream()
.map(e -> new BsonArray(
e.vectorAsList().stream()
.map(BsonDouble::new)
.toList()))
.toList();
}
/**
* Takes a single string and returns a BSON array embedding to
* use in a vector query.
*/
public static BsonArray getEmbedding(String text) {
Response<Embedding> response = getEmbeddingModel().embed(text);
return new BsonArray(
response.content().vectorAsList().stream()
.map(BsonDouble::new)
.toList());
}
}
3

EmbeddingGenerator.java 이라는 파일을 만들고 다음 코드를 붙여넣습니다.

이 코드는 getEmbeddings 메서드와 MongoDB Java 동기화 드라이버 를 사용하여 다음을 수행합니다.

  1. 로컬 Atlas 배포서버 서버 또는 Atlas cluster 에 연결합니다.

  2. 비어 있지 않은 summary 필드 가 있는 sample_airbnb.listingsAndReviews 컬렉션 에서 문서의 하위 집합을 가져옵니다.

    참고

    데모 목적으로 처리 시간을 줄이기 위해 250 문서의 limit 를 설정하다 합니다. 필요에 따라 사용 사례 에 더 적합하도록 이 제한을 조정하거나 제거 할 수 있습니다.

  3. 이전에 정의한 getEmbeddings 메서드를 사용하여 각 문서의 summary 필드 에서 임베딩을 생성합니다.

  4. 해당 임베딩 값이 포함된 새 embedding 필드 로 각 문서 를 업데이트합니다.

EmbeddingGenerator.java
import com.mongodb.MongoException;
import com.mongodb.bulk.BulkWriteResult;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.BulkWriteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.UpdateOneModel;
import com.mongodb.client.model.Updates;
import com.mongodb.client.model.WriteModel;
import org.bson.BsonArray;
import org.bson.Document;
import org.bson.conversions.Bson;
import java.util.ArrayList;
import java.util.List;
public class EmbeddingGenerator {
public static void main(String[] args) {
String uri = System.getenv("ATLAS_CONNECTION_STRING");
if (uri == null || uri.isEmpty()) {
throw new RuntimeException("ATLAS_CONNECTION_STRING env variable is not set or is empty.");
}
// establish connection and set namespace
try (MongoClient mongoClient = MongoClients.create(uri)) {
MongoDatabase database = mongoClient.getDatabase("sample_airbnb");
MongoCollection<Document> collection = database.getCollection("listingsAndReviews");
// define parameters for the find() operation
// NOTE: this example uses a limit to reduce processing time
Bson projectionFields = Projections.fields(
Projections.include("_id", "summary"));
Bson filterSummary = Filters.ne("summary", "");
int limit = 250;
try (MongoCursor<Document> cursor = collection
.find(filterSummary)
.projection(projectionFields)
.limit(limit)
.iterator()) {
List<String> summaries = new ArrayList<>();
List<String> documentIds = new ArrayList<>();
while (cursor.hasNext()) {
Document document = cursor.next();
String summary = document.getString("summary");
String id = document.get("_id").toString();
summaries.add(summary);
documentIds.add(id);
}
// generate embeddings for the summary in each document
// and add to the document to the 'embeddings' array field
System.out.println("Generating embeddings for " + summaries.size() + " documents.");
System.out.println("This operation may take up to several minutes.");
List<BsonArray> embeddings = OllamaModels.getEmbeddings(summaries);
List<WriteModel<Document>> updateDocuments = new ArrayList<>();
for (int j = 0; j < summaries.size(); j++) {
UpdateOneModel<Document> updateDoc = new UpdateOneModel<>(
Filters.eq("_id", documentIds.get(j)),
Updates.set("embeddings", embeddings.get(j)));
updateDocuments.add(updateDoc);
}
// bulk write the updated documents to the 'listingsAndReviews' collection
int result = performBulkWrite(updateDocuments, collection);
System.out.println("Added embeddings successfully to " + result + " documents.");
}
} catch (MongoException me) {
throw new RuntimeException("Failed to connect to MongoDB", me);
} catch (Exception e) {
throw new RuntimeException("Operation failed: ", e);
}
}
/**
* Performs a bulk write operation on the specified collection.
*/
private static int performBulkWrite(List<WriteModel<Document>> updateDocuments, MongoCollection<Document> collection) {
if (updateDocuments.isEmpty()) {
return 0;
}
BulkWriteResult result;
try {
BulkWriteOptions options = new BulkWriteOptions().ordered(false);
result = collection.bulkWrite(updateDocuments, options);
return result.getModifiedCount();
} catch (MongoException me) {
throw new RuntimeException("Failed to insert documents", me);
}
}
}
4

파일을 저장하고 실행합니다. 출력은 다음과 같습니다.

Generating embeddings for 250 documents.
This operation may take up to several minutes.
Added embeddings successfully to 250 documents.
1

이 예시에서는 Hugging Face 모델 허브의 mixedbread-ai/mxbai-embed-Larg-v1 모델을 사용합니다. 모델 파일을 다운로드하는 가장 간단한 방법은 Git 대용량 파일 스토리지를 사용해 Git을 통해 리포지토리를 복제하는 것입니다. Hugging Face는 리포지토리 복제 요청을 인증하기 위해 사용자 액세스 토큰 또는 Git over SSH가 필요합니다.

git clone https://<your-hugging-face-username>:<your-hugging-face-user-access-token>@huggingface.co/mixedbread-ai/mxbai-embed-large-v1
git clone git@hf.co:mixedbread-ai/mxbai-embed-large-v1

Git 대용량 파일 스토리지

Hugging Face 모델 파일은 크기가 크며, 리포지토리를 복제하려면 Git Large File Storage(git-lfs)가 필요합니다. 대용량 파일 스토리지와 관련된 오류가 표시되면 git-lfs를 설치했는지 확인하세요.

2

시스템의 로컬 모델 파일 경로를 가져옵니다. 방금 복제한 git 리포지토리가 포함된 상위 디렉터리입니다. 이 튜토리얼을 위해 생성한 프로젝트 디렉토리 내에서 모델 저장소를 복제한 경우 상위 디렉터리 경로는 다음과 유사해야 합니다.

/Users/<username>/local-rag-mongodb

모델 디렉토리를 확인하고 model_quantized.onnx 파일이 있는 onnx 디렉토리가 포함되어 있는지 확인합니다.

cd mxbai-embed-large-v1/onnx
ls
model.onnx model_fp16.onnx model_quantized.onnx
3
  1. local-rag-mongodb 상위 디렉토리로 다시 이동합니다.

  2. get-embeddings.js 파일을 생성하고 다음 코드를 붙여넣습니다.

    get-embeddings.js
    import { env, pipeline } from '@xenova/transformers';
    // Function to generate embeddings for given data
    export async function getEmbeddings(data) {
    // Replace this path with the parent directory that contains the model files
    env.localModelPath = '/Users/<username>/local-rag-mongodb/';
    env.allowRemoteModels = false;
    const task = 'feature-extraction';
    const model = 'mxbai-embed-large-v1';
    const embedder = await pipeline(
    task, model);
    const results = await embedder(data, { pooling: 'mean', normalize: true });
    return Array.from(results.data);
    }

    '/Users/<username>/local-rag-mongodb/'를 이전 단계의 로컬 경로로 교체합니다.

  3. generate-embeddings.js라는 다른 파일을 만들고 그 안에 다음 코드를 붙여넣습니다.

    generate-embeddings.js
    1import { MongoClient } from 'mongodb';
    2import { getEmbeddings } from './get-embeddings.js';
    3
    4async function run() {
    5 const client = new MongoClient(process.env.ATLAS_CONNECTION_STRING);
    6
    7 try {
    8 // Connect to your local MongoDB deployment
    9 await client.connect();
    10 const db = client.db("sample_airbnb");
    11 const collection = db.collection("listingsAndReviews");
    12
    13 const filter = { '$and': [
    14 { 'summary': { '$exists': true, '$ne': null } },
    15 { 'embeddings': { '$exists': false } }
    16 ]};
    17
    18 // This is a long-running operation for all docs in the collection,
    19 // so we limit the docs for this example
    20 const cursor = collection.find(filter).limit(50);
    21
    22 // To verify that you have the local embedding model configured properly,
    23 // try generating an embedding for one document
    24 const firstDoc = await cursor.next();
    25 if (!firstDoc) {
    26 console.log('No document found.');
    27 return;
    28 }
    29
    30 const firstDocEmbeddings = await getEmbeddings(firstDoc.summary);
    31 console.log(firstDocEmbeddings);
    32
    33 // After confirming you are successfully generating embeddings,
    34 // uncomment the following code to generate embeddings for all docs.
    35 /* cursor.rewind(); // Reset the cursor to process documents again
    36 * console.log("Generating embeddings for documents. Standby.");
    37 * let updatedDocCount = 0;
    38 *
    39 * for await (const doc of cursor) {
    40 * const text = doc.summary;
    41 * const embeddings = await getEmbeddings(text);
    42 * await collection.updateOne({ "_id": doc._id },
    43 * {
    44 * "$set": {
    45 * "embeddings": embeddings
    46 * }
    47 * }
    48 * );
    49 * updatedDocCount += 1;
    50 * }
    51 * console.log("Count of documents updated: " + updatedDocCount);
    52 */
    53 } catch (err) {
    54 console.log(err.stack);
    55 }
    56 finally {
    57 await client.close();
    58 }
    59}
    60run().catch(console.dir);

    이 코드에는 모델을 올바르게 다운로드하고 올바른 경로를 사용하고 있는지 테스트하기 위한 몇 줄이 포함되어 있습니다. 다음 명령을 실행하여 코드를 실행합니다.

    node --env-file=.env generate-embeddings.js
    Tensor {
    dims: [ 1, 1024 ],
    type: 'float32',
    data: Float32Array(1024) [
    -0.01897735893726349, -0.001120976754464209, -0.021224822849035263,
    -0.023649735376238823, -0.03350808471441269, -0.0014186901971697807,
    -0.009617107920348644, 0.03344292938709259, 0.05424851179122925,
    -0.025904450565576553, 0.029770011082291603, -0.0006215018220245838,
    0.011056603863835335, -0.018984895199537277, 0.03985185548663139,
    -0.015273082070052624, -0.03193040192127228, 0.018376577645540237,
    -0.02236943319439888, 0.01433168537914753, 0.02085157483816147,
    -0.005689046811312437, -0.05541415512561798, -0.055907104164361954,
    -0.019112611189484596, 0.02196515165269375, 0.027313007041811943,
    -0.008618313819169998, 0.045496534556150436, 0.06271681934595108,
    -0.0028660669922828674, -0.02433634363114834, 0.02016191929578781,
    -0.013882477767765522, -0.025465600192546844, 0.0000950733374338597,
    0.018200192600488663, -0.010413561016321182, -0.002004098379984498,
    -0.058351870626211166, 0.01749623566865921, -0.013926318846642971,
    -0.00278360559605062, -0.010333008132874966, 0.004406726453453302,
    0.04118744656443596, 0.02210155501961708, -0.016340743750333786,
    0.004163357429206371, -0.018561601638793945, 0.0021984230261296034,
    -0.012378614395856857, 0.026662321761250496, -0.006476820446550846,
    0.001278138137422502, -0.010084952227771282, -0.055993322283029556,
    -0.015850437805056572, 0.015145729295909405, 0.07512971013784409,
    -0.004111358895897865, -0.028162647038698196, 0.023396577686071396,
    -0.01159974467009306, 0.021751703694462776, 0.006198467221111059,
    0.014084039255976677, -0.0003913900291081518, 0.006310020107775927,
    -0.04500332102179527, 0.017774192616343498, -0.018170733004808426,
    0.026185045018792152, -0.04488714039325714, -0.048510149121284485,
    0.015152698382735252, 0.012136898003518581, 0.0405895821750164,
    -0.024783289059996605, -0.05514788627624512, 0.03484730422496796,
    -0.013530988246202469, 0.0319477915763855, 0.04537525027990341,
    -0.04497901350259781, 0.009621822275221348, -0.013845544308423996,
    0.0046155862510204315, 0.03047163411974907, 0.0058857654221355915,
    0.005858785007148981, 0.01180865429341793, 0.02734190598130226,
    0.012322399765253067, 0.03992653638124466, 0.015777742490172386,
    0.017797520384192467, 0.02265017107129097, -0.018233606591820717,
    0.02064627595245838,
    ... 924 more items
    ],
    size: 1024
    }
  4. 선택 사항으로 로컬 모델을 사용하여 임베딩을 성공적으로 생성하고 있는지 확인한 후 35~52행에서 코드의 주석 처리를 제거하여 컬렉션의 모든 문서에 대한 임베딩을 생성합니다 파일을 저장합니다.

    그런 다음 명령을 실행하여 코드를 실행합니다.

    node --env-file=.env generate-embeddings.js
    [
    Tensor {
    dims: [ 1024 ],
    type: 'float32',
    data: Float32Array(1024) [
    -0.043243519961833954, 0.01316747535020113, -0.011639945209026337,
    -0.025046885013580322, 0.005129443947225809, -0.02003324404358864,
    0.005245734006166458, 0.10105721652507782, 0.05425914749503136,
    -0.010824322700500488, 0.021903572604060173, 0.048009492456912994,
    0.01291663944721222, -0.015903260558843613, -0.008034848608076572,
    -0.003592714900150895, -0.029337648302316666, 0.02282896265387535,
    -0.029112281277775764, 0.011099508963525295, -0.012238143011927605,
    -0.008351574651896954, -0.048714976757764816, 0.001015961286611855,
    0.02252192236483097, 0.04426417499780655, 0.03514830768108368,
    -0.02088250033557415, 0.06391220539808273, 0.06896235048770905,
    -0.015386332757771015, -0.019206153228878975, 0.015263230539858341,
    -0.00019019744649995118, -0.032121095806360245, 0.015855342149734497,
    0.05055809020996094, 0.004083932377398014, 0.026945054531097412,
    -0.0505746565759182, -0.009507855400443077, -0.012497996911406517,
    0.06249537691473961, -0.04026378318667412, 0.010749109089374542,
    0.016748877242207527, -0.0235306303948164, -0.03941794112324715,
    0.027474915608763695, -0.02181144617497921, 0.0026422827504575253,
    0.005104491952806711, 0.027314607053995132, 0.019283341243863106,
    0.005245842970907688, -0.018712762743234634, -0.08618085831403732,
    0.003314188914373517, 0.008071620017290115, 0.05356570705771446,
    -0.008000597357749939, 0.006983411032706499, -0.0070550404489040375,
    -0.043323490768671036, 0.03490140289068222, 0.03810165822505951,
    0.0406375490128994, -0.0032191979698836803, 0.01489361934363842,
    -0.01609957590699196, -0.006372962612658739, 0.03360277786850929,
    -0.014810526743531227, -0.00925799086689949, -0.01885424554347992,
    0.0182492695748806, 0.009002899751067162, -0.004713123198598623,
    -0.00846288911998272, -0.012471121735870838, -0.0080558517947793,
    0.0135461101308465, 0.03335557505488396, -0.0027410900220274925,
    -0.02145615592598915, 0.01378028653562069, 0.03708091005682945,
    0.03519297018647194, 0.014239554293453693, 0.02219904027879238,
    0.0015641176141798496, 0.02624501660466194, 0.022713981568813324,
    -0.004414170514792204, 0.026919621974229813, -0.002607459668070078,
    -0.04017219692468643, -0.003570320550352335, -0.022905709221959114,
    0.030657364055514336,
    ... 924 more items
    ],
    size: 1024
    }
    ]
    Generating embeddings for documents. Standby.
    Count of documents updated: 50
1

이 코드는 다음 작업을 수행합니다.

  • 로컬 Atlas 배포서버 또는 Atlas 클러스터에 연결하고 sample_airbnb.listingsAndReviews 컬렉션을 선택합니다.

  • Hugging Face 모델 허브에서 mixedbread-ai/mxbai-embed-large-v1 모델을 로드하고 로컬에 저장합니다. 자세히 알아보려면 모델 다운로드하기를 참조하세요.

  • 모델을 사용하여 벡터 임베딩을 생성하는 함수를 정의합니다.

  • 컬렉션에 있는 문서의 하위 집합의 경우:

    • 문서의 summary 필드에서 임베딩을 생성합니다.

    • 임베딩이 포함된 새 embeddings 필드를 만들어 문서를 업데이트합니다.

    from pymongo import MongoClient
    from sentence_transformers import SentenceTransformer
    # Connect to your local Atlas deployment or Atlas Cluster
    client = MongoClient(ATLAS_CONNECTION_STRING)
    # Select the sample_airbnb.listingsAndReviews collection
    collection = client["sample_airbnb"]["listingsAndReviews"]
    # Load the embedding model (https://huggingface.co/sentence-transformers/mixedbread-ai/mxbai-embed-large-v1)
    model_path = "<model-path>"
    model = SentenceTransformer('mixedbread-ai/mxbai-embed-large-v1')
    model.save(model_path)
    model = SentenceTransformer(model_path)
    # Define function to generate embeddings
    def get_embedding(text):
    return model.encode(text).tolist()
    # Filters for only documents with a summary field and without an embeddings field
    filter = { '$and': [ { 'summary': { '$exists': True, '$ne': None } }, { 'embeddings': { '$exists': False } } ] }
    # Creates embeddings for subset of the collection
    updated_doc_count = 0
    for document in collection.find(filter).limit(50):
    text = document['summary']
    embedding = get_embedding(text)
    collection.update_one({ '_id': document['_id'] }, { "$set": { 'embeddings': embedding } }, upsert=True)
    updated_doc_count += 1
    print("Documents updated: {}".format(updated_doc_count))
    Documents updated: 50
2

이 경로는 다음과 유사해야 합니다. /Users/<username>/local-rag-mongodb

3

이 코드는 실행 데 몇 분 정도 걸릴 수 있습니다. 완료되면 mongosh 에서 로컬 배포서버 에 연결하거나 배포의 연결 문자열 을 사용하여 애플리케이션 에 연결하여 벡터 임베딩을 볼 수 있습니다. 그런 다음 sample_airbnb.listingsAndReviews 컬렉션 에서 읽기 작업을 실행 수 있습니다.

클러스터의 sample_airbnb.listingsAndReviews 컬렉션으로 이동하고 문서의 필드를 확장하여 Atlas UI에서 벡터 임베딩을 볼 수 있습니다.

Atlas에서 벡터를 효율적으로 저장하고 수집하기 위해 샘플 데이터의 임베딩을 BSON 벡터로 변환합니다. 자세한 내용은 네이티브 임베딩을 BSON 벡터로 변환하는 방법을 참조하세요.

sample_airbnb.listingsAndReviews 컬렉션에서 벡터 검색을 활성화하려면 Atlas Vector Search 인덱스을 생성하세요.

이 튜토리얼에서는 지원되는 MongoDB 드라이버 를 사용하거나 Atlas CLI 를 사용하여 프로그래밍 방식으로 Atlas Vector Search 인덱스 를 생성하는 방법을 안내합니다. Atlas Vector Search 인덱스 를 만드는 다른 방법에 대한 자세한 내용은 벡터 검색을 위해 필드를 인덱싱하는 방법을 참조하세요.

참고

Atlas Vector Search 검색 인덱스를 만들려면 Atlas 프로젝트에 대한 Project Data Access Admin 이상의 액세스 권한이 있어야 합니다.

MongoDB Go 드라이버 v1.16.0 이상을 사용하여 컬렉션의 Atlas Vector Search 인덱스를 생성하려면 다음 단계를 수행하세요.

1

vector-index.go라는 이름의 파일을 생성하고 다음 코드를 파일에 붙여넣습니다.

vector-index.go
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
ctx := context.Background()
if err := godotenv.Load(); err != nil {
log.Println("no .env file found")
}
// Connect to your Atlas cluster
uri := os.Getenv("ATLAS_CONNECTION_STRING")
if uri == "" {
log.Fatal("set your 'ATLAS_CONNECTION_STRING' environment variable.")
}
clientOptions := options.Client().ApplyURI(uri)
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatalf("failed to connect to the server: %v", err)
}
defer func() { _ = client.Disconnect(ctx) }()
// Set the namespace
coll := client.Database("sample_airbnb").Collection("listingsAndReviews")
indexName := "vector_index"
opts := options.SearchIndexes().SetName(indexName).SetType("vectorSearch")
type vectorDefinitionField struct {
Type string `bson:"type"`
Path string `bson:"path"`
NumDimensions int `bson:"numDimensions"`
Similarity string `bson:"similarity"`
}
type vectorDefinition struct {
Fields []vectorDefinitionField `bson:"fields"`
}
indexModel := mongo.SearchIndexModel{
Definition: vectorDefinition{
Fields: []vectorDefinitionField{{
Type: "vector",
Path: "embeddings",
NumDimensions: 768,
Similarity: "cosine"}},
},
Options: opts,
}
log.Println("Creating the index.")
searchIndexName, err := coll.SearchIndexes().CreateOne(ctx, indexModel)
if err != nil {
log.Fatalf("failed to create the search index: %v", err)
}
// Await the creation of the index.
log.Println("Polling to confirm successful index creation.")
log.Println("NOTE: This may take up to a minute.")
searchIndexes := coll.SearchIndexes()
var doc bson.Raw
for doc == nil {
cursor, err := searchIndexes.List(ctx, options.SearchIndexes().SetName(searchIndexName))
if err != nil {
fmt.Errorf("failed to list search indexes: %w", err)
}
if !cursor.Next(ctx) {
break
}
name := cursor.Current.Lookup("name").StringValue()
queryable := cursor.Current.Lookup("queryable").Boolean()
if name == searchIndexName && queryable {
doc = cursor.Current
} else {
time.Sleep(5 * time.Second)
}
}
log.Println("Name of Index Created: " + searchIndexName)
}

이 인덱스 정의는 sample_airbnb.listingsAndReviews 컬렉션에 대한 vectorSearch 유형의 인덱스에서 embeddings 필드를 인덱싱하는 것을 지정합니다. 이 필드에는 임베딩 모델을 사용하여 생성된 임베딩이 포함되어 있습니다. 인덱스 정의는 768 벡터 차원을 지정하고 cosine을 사용하여 유사성을 측정합니다.

2

파일을 저장한 후 터미널에서 다음 명령을 실행하여 코드를 실행합니다.

go run vector-index.go

MongoDB Java 드라이버 v5.2.0 이상을 사용하여 컬렉션의 Atlas Vector Search 인덱스를 생성하려면 다음 단계를 수행하세요.

1

VectorIndex.java 이라는 파일을 만들고 다음 코드를 붙여넣습니다.

이 코드는 MongoCollection 객체 를 가져와 다음 인덱스 정의를 사용하여 컬렉션 에 Atlas Vector Search 인덱스 를 생성하는 createSearchIndexes() 헬퍼 메서드를 호출합니다.

  • embedding 컬렉션 에 대한 vectorSearch 인덱스 sample_airbnb.listingsAndReviews 유형에서 필드 를 인덱싱합니다. 이 필드 에는 임베딩 모델을 사용하여 생성된 임베딩이 포함됩니다.

  • 768 벡터 차원을 적용하고 cosine을 사용하여 벡터 간의 유사성을 측정합니다.

VectorIndex.java
import com.mongodb.MongoException;
import com.mongodb.client.ListSearchIndexesIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.SearchIndexModel;
import com.mongodb.client.model.SearchIndexType;
import org.bson.Document;
import org.bson.conversions.Bson;
import java.util.Collections;
import java.util.List;
public class VectorIndex {
public static void main(String[] args) {
String uri = System.getenv("ATLAS_CONNECTION_STRING");
if (uri == null || uri.isEmpty()) {
throw new IllegalStateException("ATLAS_CONNECTION_STRING env variable is not set or is empty.");
}
// establish connection and set namespace
try (MongoClient mongoClient = MongoClients.create(uri)) {
MongoDatabase database = mongoClient.getDatabase("sample_airbnb");
MongoCollection<Document> collection = database.getCollection("listingsAndReviews");
// define the index details for the index model
String indexName = "vector_index";
Bson definition = new Document(
"fields",
Collections.singletonList(
new Document("type", "vector")
.append("path", "embeddings")
.append("numDimensions", 768)
.append("similarity", "cosine")));
SearchIndexModel indexModel = new SearchIndexModel(
indexName,
definition,
SearchIndexType.vectorSearch());
// create the index using the defined model
try {
List<String> result = collection.createSearchIndexes(Collections.singletonList(indexModel));
System.out.println("Successfully created a vector index named: " + result);
} catch (Exception e) {
throw new RuntimeException(e);
}
// wait for Atlas to build the index and make it queryable
System.out.println("Polling to confirm the index has completed building.");
System.out.println("It may take up to a minute for the index to build before you can query using it.");
waitForIndexReady(collection, indexName);
} catch (MongoException me) {
throw new RuntimeException("Failed to connect to MongoDB ", me);
} catch (Exception e) {
throw new RuntimeException("Operation failed: ", e);
}
}
/**
* Polls the collection to check whether the specified index is ready to query.
*/
public static void waitForIndexReady(MongoCollection<Document> collection, String indexName) throws InterruptedException {
ListSearchIndexesIterable<Document> searchIndexes = collection.listSearchIndexes();
while (true) {
try (MongoCursor<Document> cursor = searchIndexes.iterator()) {
if (!cursor.hasNext()) {
break;
}
Document current = cursor.next();
String name = current.getString("name");
boolean queryable = current.getBoolean("queryable");
if (name.equals(indexName) && queryable) {
System.out.println(indexName + " index is ready to query");
return;
} else {
Thread.sleep(500);
}
}
}
}
}
2

파일을 저장하고 실행합니다. 출력은 다음과 같습니다.

Successfully created a vector index named: [vector_index]
Polling to confirm the index has completed building.
It may take up to a minute for the index to build before you can query using it.
vector_index index is ready to query

MongoDB Node 드라이버 v6.6.0 이상을 사용하여 컬렉션에 대한 Atlas Vector Search 인덱스를 만들려면 다음 단계를 수행하세요.

1

vector-index.js라는 이름의 파일을 생성하고 다음 코드를 파일에 붙여넣습니다.

vector-index.js
import { MongoClient } from 'mongodb';
// Connect to your Atlas deployment
const client = new MongoClient(process.env.ATLAS_CONNECTION_STRING);
async function run() {
try {
const database = client.db("sample_airbnb");
const collection = database.collection("listingsAndReviews");
// Define your Atlas Vector Search index
const index = {
name: "vector_index",
type: "vectorSearch",
definition: {
"fields": [
{
"type": "vector",
"numDimensions": 1024,
"path": "embeddings",
"similarity": "cosine"
}
]
}
}
// Call the method to create the index
const result = await collection.createSearchIndex(index);
console.log(result);
} finally {
await client.close();
}
}
run().catch(console.dir);

이 인덱스 정의는 sample_airbnb.listingsAndReviews 컬렉션에 대한 vectorSearch 유형의 인덱스에서 embeddings 필드를 인덱싱하는 것을 지정합니다. 이 필드에는 임베딩 모델을 사용하여 생성된 임베딩이 포함되어 있습니다. 인덱스 정의는 1024 벡터 차원을 지정하고 cosine을 사용하여 유사성을 측정합니다.

2
  1. 파일을 저장한 후 터미널에서 다음 명령을 실행하여 코드를 실행합니다.

    node --env-file=.env vector-index.js

PyMongo 드라이버 v4.7 이상을 사용하여 컬렉션에 대한 Atlas Vector Search 인덱스를 만들려면 다음 단계를 수행하세요.

PyMongo 드라이버를 사용하여 애플리케이션에서 직접 인덱스를 생성할 수 있습니다. 다음 코드를 노트북에 붙여넣고 실행합니다.

from pymongo.operations import SearchIndexModel
# Create your index model, then create the search index
search_index_model = SearchIndexModel(
definition = {
"fields": [
{
"type": "vector",
"numDimensions": 1024,
"path": "embeddings",
"similarity": "cosine"
}
]
},
name = "vector_index",
type = "vectorSearch"
)
collection.create_search_index(model=search_index_model)

이 인덱스 정의는 sample_airbnb.listingsAndReviews 컬렉션에 대한 vectorSearch 유형의 인덱스에서 embeddings 필드를 인덱싱하는 것을 지정합니다. 이 필드에는 임베딩 모델을 사용하여 생성된 임베딩이 포함되어 있습니다. 인덱스 정의는 1024 벡터 차원을 지정하고 cosine을 사용하여 유사성을 측정합니다.

Atlas CLI 를 사용하여 Atlas Vector Search 인덱스 를 생성하려면 다음 단계를 수행하세요.

1

vector-index.json 이라는 파일 을 만들고 파일 에 다음 인덱스 정의를 붙여넣습니다.

vector-index.json
{
"database": "sample_airbnb",
"collectionName": "listingsAndReviews",
"type": "vectorSearch",
"name": "vector_index",
"fields": [
{
"type": "vector",
"path": "embeddings",
"numDimensions": 768,
"similarity": "cosine"
}
]
}

이 인덱스 정의는 다음을 지정합니다.

  • embeddings 컬렉션 에 대한 vectorSearch 인덱스 sample_airbnb.listingsAndReviews 유형에서 필드 를 인덱싱합니다. 이 필드 에는 임베딩 모델을 사용하여 생성된 임베딩이 포함됩니다.

  • 768 벡터 차원을 적용하고 cosine을 사용하여 벡터 간의 유사성을 측정합니다.

2

파일을 프로젝트 디렉토리에 저장한 후 터미널에서 다음 명령을 실행하여 <path-to-file>을 생성한 vector-index.json 파일의 경로로 바꿉니다.

atlas deployments search indexes create --file <path-to-file>

예를 예시/Users/<username>/local-rag-mongodb/vector-index.json 경로와 유사할 수 있습니다.

1

vector-index.json 이라는 파일 을 만들고 파일 에 다음 인덱스 정의를 붙여넣습니다.

vector-index.json
{
"database": "sample_airbnb",
"collectionName": "listingsAndReviews",
"type": "vectorSearch",
"name": "vector_index",
"fields": [
{
"type": "vector",
"path": "embeddings",
"numDimensions": 768,
"similarity": "cosine"
}
]
}

이 인덱스 정의는 다음을 지정합니다.

  • embeddings 컬렉션 에 대한 vectorSearch 인덱스 sample_airbnb.listingsAndReviews 유형에서 필드 를 인덱싱합니다. 이 필드 에는 임베딩 모델을 사용하여 생성된 임베딩이 포함됩니다.

  • 768 벡터 차원을 적용하고 cosine을 사용하여 벡터 간의 유사성을 측정합니다.

2

파일을 프로젝트 디렉토리에 저장한 후 터미널에서 다음 명령을 실행하여 <path-to-file>을 생성한 vector-index.json 파일의 경로로 바꿉니다.

atlas deployments search indexes create --file <path-to-file>

예를 예시/Users/<username>/local-rag-mongodb/vector-index.json 경로와 유사할 수 있습니다.

1

vector-index.json이라는 이름의 파일을 만들고 다음 인덱스 정의를 파일에 붙여넣습니다.

이 인덱스 정의는 sample_airbnb.listingsAndReviews 컬렉션에 대한 vectorSearch 유형의 인덱스에서 embeddings 필드를 인덱싱하는 것을 지정합니다. 이 필드에는 임베딩 모델을 사용하여 생성된 임베딩이 포함되어 있습니다. 인덱스 정의는 1024 벡터 차원을 지정하고 cosine을 사용하여 유사성을 측정합니다.

{
"database": "sample_airbnb",
"collectionName": "listingsAndReviews",
"type": "vectorSearch",
"name": "vector_index",
"fields": [
{
"type": "vector",
"path": "embeddings",
"numDimensions": 1024,
"similarity": "cosine"
}
]
}
2

파일을 프로젝트 디렉토리에 저장한 후 터미널에서 다음 명령을 실행하여 <path-to-file>을 생성한 vector-index.json 파일의 경로로 바꿉니다.

atlas deployments search indexes create --file <path-to-file>

이 경로는 /Users/<username>/local-rag-mongodb/vector-index.json 와 비슷해야 합니다.

1

vector-index.json이라는 이름의 파일을 만들고 다음 인덱스 정의를 파일에 붙여넣습니다.

이 인덱스 정의는 sample_airbnb.listingsAndReviews 컬렉션에 대한 vectorSearch 유형의 인덱스에서 embeddings 필드를 인덱싱하는 것을 지정합니다. 이 필드에는 임베딩 모델을 사용하여 생성된 임베딩이 포함되어 있습니다. 인덱스 정의는 1024 벡터 차원을 지정하고 cosine을 사용하여 유사성을 측정합니다.

{
"database": "sample_airbnb",
"collectionName": "listingsAndReviews",
"type": "vectorSearch",
"name": "vector_index",
"fields": [
{
"type": "vector",
"path": "embeddings",
"numDimensions": 1024,
"similarity": "cosine"
}
]
}
2

파일을 프로젝트 디렉토리에 저장한 후 터미널에서 다음 명령을 실행하여 <path-to-file>을 생성한 vector-index.json 파일의 경로로 바꿉니다.

atlas deployments search indexes create --file <path-to-file>

이 경로는 /Users/<username>/local-rag-mongodb/vector-index.json 와 비슷해야 합니다.

이 섹션에서는 Atlas Vector Search 및 Ollama를 사용하여 로컬에서 실행 수 있는 샘플 RAG 구현 을 보여 줍니다.

1
  1. common 디렉토리로 이동합니다.

    cd common
  2. retrieve-documents.go라는 파일을 만들고 다음 코드를 파일에 붙여넣습니다.

    retrieve-documents.go
    package common
    import (
    "context"
    "log"
    "os"
    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    )
    type Document struct {
    Summary string `bson:"summary"`
    ListingURL string `bson:"listing_url"`
    Score float64 `bson:"score"`
    }
    func RetrieveDocuments(query string) []Document {
    ctx := context.Background()
    if err := godotenv.Load(); err != nil {
    log.Println("no .env file found")
    }
    // Connect to your Atlas cluster
    uri := os.Getenv("ATLAS_CONNECTION_STRING")
    if uri == "" {
    log.Fatal("set your 'ATLAS_CONNECTION_STRING' environment variable.")
    }
    clientOptions := options.Client().ApplyURI(uri)
    client, err := mongo.Connect(ctx, clientOptions)
    if err != nil {
    log.Fatalf("failed to connect to the server: %v", err)
    }
    defer func() { _ = client.Disconnect(ctx) }()
    // Set the namespace
    coll := client.Database("sample_airbnb").Collection("listingsAndReviews")
    var array []string
    array = append(array, query)
    queryEmbedding := GetEmbeddings(array)
    vectorSearchStage := bson.D{
    {"$vectorSearch", bson.D{
    {"index", "vector_index"},
    {"path", "embeddings"},
    {"queryVector", queryEmbedding[0]},
    {"exact", true},
    {"limit", 5},
    }}}
    projectStage := bson.D{
    {"$project", bson.D{
    {"_id", 0},
    {"summary", 1},
    {"listing_url", 1},
    {"score", bson.D{{"$meta", "vectorSearchScore"}}},
    }}}
    cursor, err := coll.Aggregate(ctx, mongo.Pipeline{vectorSearchStage, projectStage})
    if err != nil {
    log.Fatalf("failed to retrieve data from the server: %v", err)
    }
    var results []Document
    if err = cursor.All(ctx, &results); err != nil {
    log.Fatalf("failed to unmarshal retrieved docs to model objects: %v", err)
    }
    return results
    }

    이 코드는 로컬 Atlas 배포 또는 Atlas 클러스터에서 벡터 쿼리를 수행합니다.

  3. 테스트 쿼리를 실행해 예상한 결과가 나오는지 확인합니다. 프로젝트 루트 디렉토리로 다시 이동합니다.

    cd ../
  4. test-query.go라는 새 파일을 만들고 다음 코드를 붙여넣습니다.

    테스트-쿼리.go
    package main
    import (
    "fmt"
    "local-rag-mongodb/common" // Module that contains the RetrieveDocuments function
    "log"
    "strings"
    )
    func main() {
    query := "beach house"
    matchingDocuments := common.RetrieveDocuments(query)
    if matchingDocuments == nil {
    log.Fatal("No documents matched the query.\n")
    }
    var textDocuments strings.Builder
    for _, doc := range matchingDocuments {
    // Print the contents of the matching documents for verification
    fmt.Printf("Summary: %v\n", doc.Summary)
    fmt.Printf("Listing URL: %v\n", doc.ListingURL)
    fmt.Printf("Score: %v\n", doc.Score)
    // Build a single text string to use as the context for the QA
    textDocuments.WriteString("Summary: ")
    textDocuments.WriteString(doc.Summary)
    textDocuments.WriteString("\n")
    textDocuments.WriteString("Listing URL: ")
    textDocuments.WriteString(doc.ListingURL)
    textDocuments.WriteString("\n")
    }
    fmt.Printf("\nThe constructed context for the QA follows:\n\n")
    fmt.Printf(textDocuments.String())
    }
  5. 다음 코드를 실행하여 쿼리를 실행합니다:

    go run test-query.go
    Summary: "Lani Beach House" Aloha - Please do not reserve until reading about the State Tax in "Other Things to Note" section. Please do not reserve unless you agree to pay taxes to Hawaii Beach Homes directly. If you have questions, please inquire before booking. The home has been completely redecorated in a luxurious island style: vaulted ceilings, skylights, granite counter tops, stainless steel appliances and a gourmet kitchen are just some of the the features. All bedrooms have ocean views
    Listing URL: https://www.airbnb.com/rooms/11553333
    Score: 0.85715651512146
    Summary: This peaceful house in North Bondi is 300m to the beach and a minute's walk to cafes and bars. With 3 bedrooms, (can sleep up to 8) it is perfect for families, friends and pets. The kitchen was recently renovated and a new lounge and chairs installed. The house has a peaceful, airy, laidback vibe - a perfect beach retreat. Longer-term bookings encouraged. Parking for one car. A parking permit for a second car can also be obtained on request.
    Listing URL: https://www.airbnb.com/rooms/10423504
    Score: 0.8425835371017456
    Summary: There are 2 bedrooms and a living room in the house. 1 Bathroom. 1 Kitchen. Friendly neighbourhood. Close to sea side and Historical places.
    Listing URL: https://www.airbnb.com/rooms/10488837
    Score: 0.8403302431106567
    Summary: Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! This spacious 4 Bedroom and 4 Bath house has all you need for your family or group. Perfect for Family Vacations and executive retreats. We are in a gated beachfront estate, with lots of space for your activities.
    Listing URL: https://www.airbnb.com/rooms/10317142
    Score: 0.8367050886154175
    Summary: This is a gorgeous home just off the main rd, with lots of sun and new amenities. room has own entrance with small deck, close proximity to the beach , bus to the junction , around the corner form all the cafes, bars and restaurants (2 mins).
    Listing URL: https://www.airbnb.com/rooms/11719579
    Score: 0.8262639045715332
    The constructed context for the QA follows:
    Summary: "Lani Beach House" Aloha - Please do not reserve until reading about the State Tax in "Other Things to Note" section. Please do not reserve unless you agree to pay taxes to Hawaii Beach Homes directly. If you have questions, please inquire before booking. The home has been completely redecorated in a luxurious island style: vaulted ceilings, skylights, granite counter tops, stainless steel appliances and a gourmet kitchen are just some of the the features. All bedrooms have ocean views
    Listing URL: https://www.airbnb.com/rooms/11553333
    Summary: This peaceful house in North Bondi is 300m to the beach and a minute's walk to cafes and bars. With 3 bedrooms, (can sleep up to 8) it is perfect for families, friends and pets. The kitchen was recently renovated and a new lounge and chairs installed. The house has a peaceful, airy, laidback vibe - a perfect beach retreat. Longer-term bookings encouraged. Parking for one car. A parking permit for a second car can also be obtained on request.
    Listing URL: https://www.airbnb.com/rooms/10423504
    Summary: There are 2 bedrooms and a living room in the house. 1 Bathroom. 1 Kitchen. Friendly neighbourhood. Close to sea side and Historical places.
    Listing URL: https://www.airbnb.com/rooms/10488837
    Summary: Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! This spacious 4 Bedroom and 4 Bath house has all you need for your family or group. Perfect for Family Vacations and executive retreats. We are in a gated beachfront estate, with lots of space for your activities.
    Listing URL: https://www.airbnb.com/rooms/10317142
    Summary: This is a gorgeous home just off the main rd, with lots of sun and new amenities. room has own entrance with small deck, close proximity to the beach , bus to the junction , around the corner form all the cafes, bars and restaurants (2 mins).
    Listing URL: https://www.airbnb.com/rooms/11719579
2

다음 명령을 실행하여 생성 모델을 가져옵니다.

ollama pull mistral
3

local-llm.go라는 파일을 만들고 다음 코드를 붙여넣습니다.

local-llm.go
package main
import (
"context"
"local-rag-mongodb/common" // Module that contains the RetrieveDocuments function
"log"
"strings"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/ollama"
"github.com/tmc/langchaingo/prompts"
)
func main() {
// Retrieve documents from the collection that match the query
const query = "beach house"
matchingDocuments := common.RetrieveDocuments(query)
if matchingDocuments == nil {
log.Fatalf("no documents matched the query %q", query)
}
// Generate the text string from the matching documents to pass to the
// LLM as context to answer the question
var textDocuments strings.Builder
for _, doc := range matchingDocuments {
textDocuments.WriteString("Summary: ")
textDocuments.WriteString(doc.Summary)
textDocuments.WriteString("\n")
textDocuments.WriteString("Listing URL: ")
textDocuments.WriteString(doc.ListingURL)
textDocuments.WriteString("\n")
}
// Have the LLM answer the question using the provided context
llm, err := ollama.New(ollama.WithModel("mistral"))
if err != nil {
log.Fatalf("failed to initialize the Ollama Mistral model client: %v", err)
}
const question = `Can you recommend me a few AirBnBs that are beach houses?
Include a link to the listings.`
template := prompts.NewPromptTemplate(
`Use the following pieces of context to answer the question at the end.
Context: {{.context}}
Question: {{.question}}`,
[]string{"context", "question"},
)
prompt, err := template.Format(map[string]any{
"context": textDocuments.String(),
"question": question,
})
ctx := context.Background()
completion, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt)
if err != nil {
log.Fatalf("failed to generate a response from the given prompt: %q", prompt)
}
log.Println("Response: ", completion)
}

이 코드는 다음을 수행합니다.

  • 쿼리 문자열에 대한 임베딩을 생성합니다.

  • 관련 문서를 쿼리합니다.

  • LLM을 프롬프트하고 응답을 반환합니다. 생성된 응답은 다를 수 있습니다.

다음 코드를 실행하여 RAG 구현을 완료합니다.

go run local-llm.go
2024/10/09 10:34:02 Response: Based on the context provided, here are some Airbnb listings for beach houses that you might find interesting:
1. Lani Beach House (Hawaii) - [Link](https://www.airbnb.com/rooms/11553333)
2. Peaceful North Bondi House (Australia) - [Link](https://www.airbnb.com/rooms/10423504)
3. Ocean Living! Secluded Secret Beach! (Florida, USA) - [Link](https://www.airbnb.com/rooms/10317142)
4. Gorgeous Home just off the main road (California, USA) - [Link](https://www.airbnb.com/rooms/11719579)

이 섹션에서는 Atlas Vector Search 및 Ollama를 사용하여 로컬에서 실행 수 있는 샘플 RAG 구현 을 보여 줍니다.

1

LocalLLM.java(이)라는 새 파일 을 만들고 다음 코드를 붙여넣습니다.

이 코드는 getEmbeddingretrieveDocuments 메서드와 Ollama chatmodel 를 사용하여 다음을 수행합니다.

  1. 로컬 Atlas 배포서버 서버 또는 Atlas cluster 에 연결

  2. 이전에 정의한 getEmbedding 메서드를 사용하여 쿼리 문자열에 대한 임베딩을 생성합니다.

  3. retrieveDocuments 메서드를 사용하여 관련 문서에 대한 컬렉션 을 쿼리합니다.

    쿼리 에는 listing_url, summary 및 벡터 score 필드만 반환하는 프로젝션 단계가 있는 집계 파이프라인 이 포함되어 있습니다. 데이터와 사용 사례 에 더 적합하도록 이 파이프라인 을 수정하거나 제거 할 수 있습니다.

  4. createPrompt 메서드를 사용하여 조회된 문서와 질문을 연결하여 컨텍스트를 만듭니다.

  5. 생성된 프롬프트를 이전에 정의한 LLM 에 chatmodel 피드하여 응답을 생성합니다.

  6. 질문과 생성된 응답을 콘솔에 출력합니다.

    참고

    데모 목적으로 컨텍스트 정보가 포함된 프롬프트도 출력합니다. 프로덕션 환경에서는 이 줄을 제거 해야 합니다.

LocalLLM.java
import com.mongodb.MongoException;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.search.FieldSearchPath;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.ollama.OllamaChatModel;
import org.bson.BsonArray;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.conversions.Bson;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.mongodb.client.model.Aggregates.project;
import static com.mongodb.client.model.Aggregates.vectorSearch;
import static com.mongodb.client.model.Projections.exclude;
import static com.mongodb.client.model.Projections.fields;
import static com.mongodb.client.model.Projections.include;
import static com.mongodb.client.model.Projections.metaVectorSearchScore;
import static com.mongodb.client.model.search.SearchPath.fieldPath;
import static com.mongodb.client.model.search.VectorSearchOptions.exactVectorSearchOptions;
import static java.util.Arrays.asList;
public class LocalLLM {
// User input: the question to answer
static String question = "Can you recommend me a few AirBnBs that are beach houses? Include a link to the listings.";
public static void main(String[] args) {
String uri = System.getenv("ATLAS_CONNECTION_STRING");
if (uri == null || uri.isEmpty()) {
throw new IllegalStateException("ATLAS_CONNECTION_STRING env variable is not set or is empty.");
}
// establish connection and set namespace
try (MongoClient mongoClient = MongoClients.create(uri)) {
MongoDatabase database = mongoClient.getDatabase("sample_airbnb");
MongoCollection<Document> collection = database.getCollection("listingsAndReviews");
// generate a response to the user question
System.out.println("Question: " + question);
try {
createPrompt(question, collection);
} catch (Exception e) {
throw new RuntimeException("An error occurred while generating the response: ", e);
}
} catch (MongoException me) {
throw new RuntimeException("Failed to connect to MongoDB ", me);
} catch (Exception e) {
throw new RuntimeException("Operation failed: ", e);
}
}
/**
* Returns a list of documents from the specified MongoDB collection that
* match the user's question.
* NOTE: Update or omit the projection stage to change the desired fields in the response
*/
public static List<Document> retrieveDocuments(String question, MongoCollection<Document> collection) {
try {
// generate the query embedding to use in the vector search
BsonArray queryEmbeddingBsonArray = OllamaModels.getEmbedding(question);
List<Double> queryEmbedding = new ArrayList<>();
for (BsonValue value : queryEmbeddingBsonArray.stream().toList()) {
queryEmbedding.add(value.asDouble().getValue());
}
// define the pipeline stages for the vector search index
String indexName = "vector_index";
FieldSearchPath fieldSearchPath = fieldPath("embeddings");
int limit = 5;
List<Bson> pipeline = asList(
vectorSearch(
fieldSearchPath,
queryEmbedding,
indexName,
limit,
exactVectorSearchOptions()),
project(
fields(
exclude("_id"),
include("listing_url"),
include("summary"),
metaVectorSearchScore("score"))));
// run the query and return the matching documents
List<Document> matchingDocuments = new ArrayList<>();
collection.aggregate(pipeline).forEach(matchingDocuments::add);
return matchingDocuments;
} catch (Exception e) {
System.err.println("Error occurred while retrieving documents: " + e.getMessage());
return new ArrayList<>();
}
}
/**
* Creates a templated prompt using the question and retrieved documents, then generates
* a response using the local Ollama chat model.
*/
public static void createPrompt(String question, MongoCollection<Document> collection) {
// Retrieve documents matching the user's question
List<Document> retrievedDocuments = retrieveDocuments(question, collection);
if (retrievedDocuments.isEmpty()) {
System.out.println("No relevant documents found. Unable to generate a response.");
return;
} else
System.out.println("Generating a response from the retrieved documents. This may take a few moments.");
// Create a prompt template
OllamaChatModel ollamaChatModel = OllamaModels.getChatModel();
PromptTemplate promptBuilder = PromptTemplate.from("""
Use the following pieces of context to answer the question at the end:
{{information}}
---------------
{{question}}
""");
// build the information string from the retrieved documents
StringBuilder informationBuilder = new StringBuilder();
for (int i = 0; i < retrievedDocuments.size(); i++) {
Document doc = retrievedDocuments.get(i);
String listingUrl = doc.getString("listing_url");
String summary = doc.getString("summary");
informationBuilder.append("Listing URL: ").append(listingUrl)
.append("\nSummary: ").append(summary)
.append("\n\n");
}
String information = informationBuilder.toString();
Map<String, Object> variables = new HashMap<>();
variables.put("question", question);
variables.put("information", information);
// generate and output the response from the chat model
Prompt prompt = promptBuilder.apply(variables);
AiMessage response = ollamaChatModel.generate(prompt.toUserMessage()).content();
System.out.println("Answer: " + response.text());
// display the filled-in prompt and context information
// NOTE: included for demonstration purposes only
System.out.println("______________________");
System.out.println("Final Prompt Sent to LLM:");
System.out.println(prompt.text());
System.out.println("______________________");
System.out.println("Number of documents in context: " + retrievedDocuments.size());
}
}
2

다음 명령을 실행하여 생성 모델을 가져옵니다.

ollama pull mistral
3

파일 을 저장하고 실행 하여 RAG 구현 을 완료합니다. 생성된 응답은 다를 수 있지만 출력은 다음과 유사하지만,

응답 출력
Question: Can you recommend me a few AirBnBs that are beach houses? Include a link to the listings.
Generating a response from the retrieved documents. This may take a few moments.
Answer: Based on the context provided, here are some beach house Airbnb listings that might suit your needs:
1. Lani Beach House - Aloha: This luxurious beach house offers ocean views from all bedrooms and features vaulted ceilings, skylights, granite countertops, stainless steel appliances, and a gourmet kitchen. You can find it at this link: https://www.airbnb.com/rooms/11553333
2. Ocean Living! Secluded Secret Beach!: This spacious 4-bedroom, 4-bath beach house is perfect for families or groups and is less than 20 steps from the ocean. It's located in a gated beachfront estate with lots of space for activities. You can find it at this link: https://www.airbnb.com/rooms/10317142
3. A beautiful and comfortable 1-Bedroom Condo in Makaha Valley: This condo offers stunning ocean and mountain views, a full kitchen, large bathroom, and is suited for longer stays. The famous Makaha Surfing Beach is not even a mile away. You can find it at this link: https://www.airbnb.com/rooms/10266175
4. There are 2 bedrooms and a living room in the house: This listing does not provide much information about the beach, but it mentions that the house is close to the sea side and historical places. You can find it at this link: https://www.airbnb.com/rooms/10488837
5. The Apartment on Copacabana beach block: This apartment is well-located, a 5-minute walk from Ipanema beach, and offers all the amenities of home, including a kitchen, washing machine, and several utensils for use. You can find it at this link: https://www.airbnb.com/rooms/10038496
______________________
Final Prompt Sent to LLM:
Use the following pieces of context to answer the question at the end:
Listing URL: https://www.airbnb.com/rooms/11553333
Summary: "Lani Beach House" Aloha - Please do not reserve until reading about the State Tax in "Other Things to Note" section. Please do not reserve unless you agree to pay taxes to Hawaii Beach Homes directly. If you have questions, please inquire before booking. The home has been completely redecorated in a luxurious island style: vaulted ceilings, skylights, granite counter tops, stainless steel appliances and a gourmet kitchen are just some of the the features. All bedrooms have ocean views
Listing URL: https://www.airbnb.com/rooms/10317142
Summary: Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! This spacious 4 Bedroom and 4 Bath house has all you need for your family or group. Perfect for Family Vacations and executive retreats. We are in a gated beachfront estate, with lots of space for your activities.
Listing URL: https://www.airbnb.com/rooms/10266175
Summary: A beautiful and comfortable 1 Bedroom Air Conditioned Condo in Makaha Valley - stunning Ocean & Mountain views All the amenities of home, suited for longer stays. Full kitchen & large bathroom. Several gas BBQ's for all guests to use & a large heated pool surrounded by reclining chairs to sunbathe. The Ocean you see in the pictures is not even a mile away, known as the famous Makaha Surfing Beach. Golfing, hiking,snorkeling paddle boarding, surfing are all just minutes from the front door.
Listing URL: https://www.airbnb.com/rooms/10488837
Summary: There are 2 bedrooms and a living room in the house. 1 Bathroom. 1 Kitchen. Friendly neighbourhood. Close to sea side and Historical places.
Listing URL: https://www.airbnb.com/rooms/10038496
Summary: The Apartment has a living room, toilet, bedroom (suite) and American kitchen. Well located, on the Copacabana beach block a 05 Min. walk from Ipanema beach (Arpoador). Internet wifi, cable tv, air conditioning in the bedroom, ceiling fans in the bedroom and living room, kitchen with microwave, cooker, Blender, dishes, cutlery and service area with fridge, washing machine, clothesline for drying clothes and closet with several utensils for use. The property boasts 45 m2.
---------------
Can you recommend me a few AirBnBs that are beach houses? Include a link to the listings.
______________________
Number of documents in context: 5

이 섹션에서는 Atlas Vector Search 및 GPT4All을 사용하여 로컬에서 실행 수 있는 샘플 RAG 구현 을 보여 줍니다.

1

retrieve-documents.js라는 파일을 만들고 다음 코드를 파일에 붙여넣습니다.

retrieve-documents.js
import { MongoClient } from 'mongodb';
import { getEmbeddings } from './get-embeddings.js';
// Function to get the results of a vector query
export async function getQueryResults(query) {
// Connect to your Atlas cluster
const client = new MongoClient(process.env.ATLAS_CONNECTION_STRING);
try {
// Get embeddings for a query
const queryEmbeddings = await getEmbeddings(query);
await client.connect();
const db = client.db("sample_airbnb");
const collection = db.collection("listingsAndReviews");
const pipeline = [
{
$vectorSearch: {
index: "vector_index",
queryVector: queryEmbeddings,
path: "embeddings",
exact: true,
limit: 5
}
}, {
$project: {
_id: 0,
summary: 1,
listing_url: 1,
score: {
$meta: "vectorSearchScore"
}
}
}
];
// Retrieve documents from Atlas using this Vector Search query
const result = collection.aggregate(pipeline);
const arrayOfQueryDocs = [];
for await (const doc of result) {
arrayOfQueryDocs.push(doc);
}
return arrayOfQueryDocs;
} catch (err) {
console.log(err.stack);
}
finally {
await client.close();
}
}

이 코드는 로컬 Atlas 배포 또는 Atlas 클러스터에서 벡터 쿼리를 수행합니다.

예상한 결과가 나오는지 확인하기 위해 테스트 쿼리를 실행하세요. test-query.js라는 새 파일을 만들고 다음 코드를 붙여넣습니다.

다음 코드를 실행하여 쿼리를 실행합니다:

node --env-file=.env test-query.js
{
listing_url: 'https://www.airbnb.com/rooms/10317142',
summary: 'Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! This spacious 4 Bedroom and 4 Bath house has all you need for your family or group. Perfect for Family Vacations and executive retreats. We are in a gated beachfront estate, with lots of space for your activities.',
score: 0.8703486323356628
}
{
listing_url: 'https://www.airbnb.com/rooms/10488837',
summary: 'There are 2 bedrooms and a living room in the house. 1 Bathroom. 1 Kitchen. Friendly neighbourhood. Close to sea side and Historical places.',
score: 0.861828088760376
}
{
listing_url: 'https://www.airbnb.com/rooms/11719579',
summary: 'This is a gorgeous home just off the main rd, with lots of sun and new amenities. room has own entrance with small deck, close proximity to the beach , bus to the junction , around the corner form all the cafes, bars and restaurants (2 mins).',
score: 0.8616757392883301
}
{
listing_url: 'https://www.airbnb.com/rooms/12657285',
summary: 'This favourite home offers a huge balcony, lots of space, easy life, all the comfort you need and a fantastic location! The beach is only 3 minutes away. Metro is 2 blocks away (starting august 2016).',
score: 0.8583258986473083
}
{
listing_url: 'https://www.airbnb.com/rooms/10985735',
summary: '5 minutes to seaside where you can swim, and 5 minutes to the woods, this two floors single house contains a cultivated garden with fruit trees, two large bedrooms and a big living room with a large sea view.',
score: 0.8573609590530396
}
2
  1. 다음 버튼을 클릭하여 GPT4All에서 Mistral 7B 모델을 다운로드하세요. 다른 모델을 살펴보려면 GPT4All 웹사이트를 참조하세요.

    다운로드
  2. 이 모델을 local-rag-mongodb 프로젝트 디렉토리로 이동합니다.

  3. 프로젝트 디렉토리에서 모델 정보가 포함된 파일을 다운로드합니다.

    curl -L https://gpt4all.io/models/models3.json -o ./models3.json
3

local-llm.js라는 파일을 만들고 다음 코드를 붙여넣습니다.

local-llm.js
import { loadModel, createCompletionStream } from "gpt4all";
import { getQueryResults } from './retrieve-documents.js';
async function run() {
try {
const query = "beach house";
const documents = await getQueryResults(query);
let textDocuments = "";
documents.forEach(doc => {
const summary = doc.summary;
const link = doc.listing_url;
const string = `Summary: ${summary} Link: ${link}. \n`
textDocuments += string;
});
const model = await loadModel(
"mistral-7b-openorca.gguf2.Q4_0.gguf", {
verbose: true,
allowDownload: false,
modelConfigFile: "./models3.json"
}
);
const question = "Can you recommend me a few AirBnBs that are beach houses? Include a link to the listings.";
const prompt = `Use the following pieces of context to answer the question at the end.
{${textDocuments}}
Question: {${question}}`;
process.stdout.write("Output: ");
const stream = createCompletionStream(model, prompt);
stream.tokens.on("data", (data) => {
process.stdout.write(data);
});
//wait till stream finishes.
await stream.result;
process.stdout.write("\n");
model.dispose();
console.log("\n Source documents: \n");
console.log(textDocuments);
} catch (err) {
console.log(err.stack);
}
}
run().catch(console.dir);

이 코드는 다음을 수행합니다.

  • 쿼리 문자열에 대한 임베딩을 생성합니다.

  • 관련 문서를 쿼리합니다.

  • LLM을 프롬프트하고 응답을 반환합니다. 생성된 응답은 다를 수 있습니다.

다음 코드를 실행하여 RAG 구현을 완료합니다.

node --env-file=.env local-llm.js
Found mistral-7b-openorca.gguf2.Q4_0.gguf at /Users/dachary.carey/.cache/gpt4all/mistral-7b-openorca.gguf2.Q4_0.gguf
Creating LLModel: {
llmOptions: {
model_name: 'mistral-7b-openorca.gguf2.Q4_0.gguf',
model_path: '/Users/dachary.carey/.cache/gpt4all',
library_path: '/Users/dachary.carey/temp/local-rag-mongodb/node_modules/gpt4all/runtimes/darwin/native;/Users/dachary.carey/temp/local-rag-mongodb',
device: 'cpu',
nCtx: 2048,
ngl: 100
},
modelConfig: {
systemPrompt: '<|im_start|>system\n' +
'You are MistralOrca, a large language model trained by Alignment Lab AI.\n' +
'<|im_end|>',
promptTemplate: '<|im_start|>user\n%1<|im_end|>\n<|im_start|>assistant\n%2<|im_end|>\n',
order: 'e',
md5sum: 'f692417a22405d80573ac10cb0cd6c6a',
name: 'Mistral OpenOrca',
filename: 'mistral-7b-openorca.gguf2.Q4_0.gguf',
filesize: '4108928128',
requires: '2.7.1',
ramrequired: '8',
parameters: '7 billion',
quant: 'q4_0',
type: 'Mistral',
description: '<strong>Strong overall fast chat model</strong><br><ul><li>Fast responses</li><li>Chat based model</li><li>Trained by Mistral AI<li>Finetuned on OpenOrca dataset curated via <a href="https://atlas.nomic.ai/">Nomic Atlas</a><li>Licensed for commercial use</ul>',
url: 'https://gpt4all.io/models/gguf/mistral-7b-openorca.gguf2.Q4_0.gguf',
path: '/Users/dachary.carey/.cache/gpt4all/mistral-7b-openorca.gguf2.Q4_0.gguf'
}
}
Output: Yes, here are a few AirBnB beach houses with links to the listings:
1. Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! - https://www.airbnb.com/rooms/10317142
2. 2 Bedrooms and a living room in the house. 1 Bathroom. 1 Kitchen. Friendly neighbourhood. Close to sea side and Historical places - https://www.airbnb.com/rooms/10488837
3. Gorgeous home just off the main rd, with lots of sun and new amenities. Room has own entrance with small deck, close proximity to the beach - https://www.airbnb.com/rooms/11719579
4. This favourite home offers a huge balcony, lots of space, easy life, all the comfort you need and a fantastic location! The beach is only 3 minutes away. Metro is 2 blocks away (starting august 2016) - https://www.airbnb.com/rooms/12657285
5. 5 minutes to seaside where you can swim, and 5 minutes to the woods, this two floors single house contains a cultivated garden with fruit trees, two large bedrooms and a big living room with a large sea view - https://www.airbnb.com/rooms/10985735
Source documents:
Summary: Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! This spacious 4 Bedroom and 4 Bath house has all you need for your family or group. Perfect for Family Vacations and executive retreats. We are in a gated beachfront estate, with lots of space for your activities. Link: https://www.airbnb.com/rooms/10317142.
Summary: There are 2 bedrooms and a living room in the house. 1 Bathroom. 1 Kitchen. Friendly neighbourhood. Close to sea side and Historical places. Link: https://www.airbnb.com/rooms/10488837.
Summary: This is a gorgeous home just off the main rd, with lots of sun and new amenities. room has own entrance with small deck, close proximity to the beach , bus to the junction , around the corner form all the cafes, bars and restaurants (2 mins). Link: https://www.airbnb.com/rooms/11719579.
Summary: This favourite home offers a huge balcony, lots of space, easy life, all the comfort you need and a fantastic location! The beach is only 3 minutes away. Metro is 2 blocks away (starting august 2016). Link: https://www.airbnb.com/rooms/12657285.
Summary: 5 minutes to seaside where you can swim, and 5 minutes to the woods, this two floors single house contains a cultivated garden with fruit trees, two large bedrooms and a big living room with a large sea view. Link: https://www.airbnb.com/rooms/10985735.

이 섹션에서는 Atlas Vector Search 및 GPT4All을 사용하여 로컬에서 실행 수 있는 샘플 RAG 구현 을 보여 줍니다.

노트북에서 다음 코드 스니펫을 실행합니다.

1

이 단계에서는 샘플 벡터 검색 쿼리를 실행하는 get_query_results라는 검색 함수를 만듭니다. 이 함수는 검색 쿼리에서 임베딩을 생성하기 위해 get_embedding 함수를 사용합니다. 그런 다음 쿼리를 실행하여 의미적으로 유사한 문서를 반환합니다.

자세한 내용은 벡터 검색 쿼리 실행을 참조하세요.

# Function to get the results of a vector search query
def get_query_results(query):
query_embedding = get_embedding(query)
pipeline = [
{
"$vectorSearch": {
"index": "vector_index",
"queryVector": query_embedding,
"path": "embeddings",
"exact": True,
"limit": 5
}
}, {
"$project": {
"_id": 0,
"summary": 1,
"listing_url": 1,
"score": {
"$meta": "vectorSearchScore"
}
}
}
]
results = collection.aggregate(pipeline)
array_of_results = []
for doc in results:
array_of_results.append(doc)
return array_of_results

함수가 관련 문서를 반환하는지 확인하려면 다음 코드를 실행하여 beach house를 쿼리합니다.

참고

환경의 차이로 인해 임베딩에 약간의 변화가 생겨 결과물이 달라질 수 있습니다.

import pprint
pprint.pprint(get_query_results("beach house"))
[{'listing_url': 'https://www.airbnb.com/rooms/10317142',
'score': 0.84868323802948,
'summary': 'Ocean Living! Secluded Secret Beach! Less than 20 steps to the '
'Ocean! This spacious 4 Bedroom and 4 Bath house has all you need '
'for your family or group. Perfect for Family Vacations and '
'executive retreats. We are in a gated beachfront estate, with '
'lots of space for your activities.'},
{'listing_url': 'https://www.airbnb.com/rooms/10488837',
'score': 0.8457906246185303,
'summary': 'There are 2 bedrooms and a living room in the house. 1 Bathroom. '
'1 Kitchen. Friendly neighbourhood. Close to sea side and '
'Historical places.'},
{'listing_url': 'https://www.airbnb.com/rooms/10423504',
'score': 0.830578088760376,
'summary': 'This peaceful house in North Bondi is 300m to the beach and a '
"minute's walk to cafes and bars. With 3 bedrooms, (can sleep up "
'to 8) it is perfect for families, friends and pets. The kitchen '
'was recently renovated and a new lounge and chairs installed. '
'The house has a peaceful, airy, laidback vibe - a perfect beach '
'retreat. Longer-term bookings encouraged. Parking for one car. A '
'parking permit for a second car can also be obtained on '
'request.'},
{'listing_url': 'https://www.airbnb.com/rooms/10548991',
'score': 0.8174338340759277,
'summary': 'Newly furnished two story home. The upstairs features a full '
...
{'listing_url': 'https://www.airbnb.com/rooms/10186755',
'score': 0.8083034157752991,
'summary': 'Near to underground metro station. Walking distance to seaside. '
'2 floors 1 entry. Husband, wife, girl and boy is living.'}]
2
  1. 다음 버튼을 클릭하여 GPT4All에서 Mistral 7B 모델을 다운로드하세요. 다른 모델을 살펴보려면 GPT4All 웹사이트를 참조하세요.

    다운로드
  2. 이 모델을 local-rag-mongodb 프로젝트 디렉토리로 이동합니다.

  3. 노트북에서 다음 코드를 실행하여 로컬 LLM을 로드합니다.

    from gpt4all import GPT4All
    local_llm_path = "./mistral-7b-openorca.gguf2.Q4_0.gguf"
    local_llm = GPT4All(local_llm_path)
3

다음 코드를 실행하여 RAG 구현을 완료합니다. 이 코드는 다음을 수행합니다.

  • 방금 정의한 함수를 사용하여 컬렉션에서 관련 문서를 쿼리합니다.

  • 조회된 문서를 컨텍스트로 사용하여 LLM에 메시지를 표시합니다. 생성된 응답은 다를 수 있습니다.

question = "Can you recommend a few AirBnBs that are beach houses? Include a link to the listing."
documents = get_query_results(question)
text_documents = ""
for doc in documents:
summary = doc.get("summary", "")
link = doc.get("listing_url", "")
string = f"Summary: {summary} Link: {link}. \n"
text_documents += string
prompt = f"""Use the following pieces of context to answer the question at the end.
{text_documents}
Question: {question}
"""
response = local_llm.generate(prompt)
cleaned_response = response.replace('\\n', '\n')
print(cleaned_response)
Answer: Yes, I can recommend a few AirBnB listings that are beach houses. Here they are with their respective links:
1. Ocean Living! Secluded Secret Beach! Less than 20 steps to the Ocean! (https://www.airbnb.com/rooms/10317142)
2. Beautiful and comfortable 1 Bedroom Air Conditioned Condo in Makaha Valley - stunning Ocean & Mountain views (https://www.airbnb.com/rooms/10266175)
3. Peaceful house in North Bondi, close to the beach and cafes (https://www.airbnb.com/rooms/10423504)

돌아가기

하이브리드 검색