본문 바로가기

NestJS + Qdrant 사용해보자 !!

@정소민fan2025. 8. 8. 15:49

벡터 DB란 무엇인가?

Qdrant는 벡터 DB이다. 벡터 DB란 어떠한 텍스트, 이미지 등의 정보를 임베딩해서 고차원의 점으로 만들어 저장해두는 DB를 말한다. 벡터 DB는 자연어나 이미지 같은 비정형 정보를 검색하는데 탁월한 성능을 발휘한다.

임베딩은?

임베딩이란 기계가 이해하지 못하는 자연어나 이미지 같은 비정형 데이터를 고정된 크기의 수치 벡터로 바꾸는 것이다.

여기서는 사전 학습된 모델이 사용되어 의미가 유사한 정보들은 유사한 수치의 벡터로 바꾸어준다.

예를 들어 "강아지"와 "고양이"는 동물이라는 카테고리로 묶여 서로 유사한 벡터로 변환될 것이다. 

하지만 임베딩을 해주는건 임베딩 모델이 해주는 일이므로 모델을 고르는 것도 프로젝트의 의도에 따라 다르게 골라야할 것이다.

Qdrant 설치

qdrant는 cloud 방식 또는 docker 로 직접 설치하는 방식으로 사용할 수 있다.

cloud 방식으로는 free cloud도 얻을 수 있지만, 서울 지역의 cloud는 얻을 수 없다. 여기서는 docker 방식으로 설치해보겠다.

cloud로 하고 싶다면 공식 사이트

 

docker 설치 방식은 쉽다. 터미널에

docker run -p 6333:6333 qdrant/qdrant

이 명령어만 입력해주면 끝

우리는 qdrant에 접속하기 위해 6333 포트를 사용할 것이다

 

그리고 nestJS가 설치되어있는 프로젝트에서

npm install @qdrant/js-client-rest

으로 라이브러리를 설치해주자

NestJS에서 Qdrant 사용하기

가장 먼저 Qdrant와 nestJS를 연결해주자. 아까 6333 포트에 컨테이너를 연결해놨으므로 localhost:6333으로 연결한다.

  private readonly client = new QdrantClient({
    url: 'http://localhost:6333',
  });

Qdrant에서 RDB에 데이터베이스에 대응되는 개념은 collection이다. collection을 만들기 위해서는 다음과 같이 코드를 작성하자.

private readonly collection = 'collection1';

  constructor() {
    this.ensureCollection();
  }

  private async ensureCollection() {
    try {
      await this.client.getCollection(this.collection);
    } catch {
      await this.client.createCollection(this.collection, {
        vectors: { size: 768, distance: 'Cosine' },
      });
    }
  }

먼저 만들어줄 collection의 이름을 정해주고, getCollection으로 해당 collection이 qdrant내에 존재하는지 확인한다. 그리고 없다면 에러를 낼 것이기에 이를 catch해서 createCollection으로 collection을 만들어주자. 이 때, vectors에 size와 distance를 넘겨줄 수 있는데, size는 저장될 벡터 문서들의 차원 수, distance는 문서 간의 거리를 측정할 방식이다. 여기서는 768차원, 코사인 유사도를 사용할 것이다.

 

그러면 본격적으로 CRUD를 사용해보기 전에, 임베딩 모델을 먼저 간단하게 만들어보자.

from flask import Flask, request, jsonify
from sentence_transformers import SentenceTransformer

app = Flask(__name__)
# jhgan/ko-sroberta-multitask 모델 로드
model = SentenceTransformer("jhgan/ko-sroberta-multitask")


@app.route("/embed", methods=["POST"])
def embed():
    data = request.get_json()
    input_text = data["text"]

    print(f"input : {input_text}")

    embedding = model.encode(
        input_text, normalize_embeddings=True)
    return jsonify({"embedding": embedding.tolist()})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

위 코드는 허깅페이스에서 한국어로 사전 학습된 임베딩 모델을 flask를 사용해서 5001번 포트에 띄우는 코드이다. 모델은 물론 사용자의 입맛에 맞게 바꾸어서 써도 된다. 이 코드에서는 리퀘스트에서 text만 꺼내어 사용한다.

 

이제 임베딩 모델도 만들었으니, 임베딩 코드를 구현해보자.

먼저 axios를 설치해보자

npm install axios
  async embed(text: string): Promise<number[]> {
    const embed_url = 'http://localhost:5001';
    const response = await axios.post(embed_url, {
      text: [text],
    });
    return response.data.embedding[0];
  }

 

이렇게 코드를 짜면? 위에서 만든 임베딩 모델로 text를 받아서 임베딩한 벡터를 응답으로 받을 수 있다. 이 벡터를 다시 qdrant에 저장하기만 하면 된다.

저장

저장할때는 QdrantClient에서 제공하는 upsert함수를 사용하면 된다.

    await this.client.upsert(this.collection, {
      wait: true,
      points: [{ id, vector, payload }],
    });

여기서 id는 보통 uuid로 만들어서 저장하는 것이 권장되고, vector는 아까 임베딩된 텍스트의 벡터 정보를 넣어주자. 그리고 payload는 키:값 쌍들로 이루어진 객체를 넘겨주어서 여러개의 정보를 저장해둘 수 있다. 사용 예시는 다음과 같다

  async create(dto: CreateDiaryDto) {
    const vector = await this.embedder.embed(dto.text); //임베딩 모델로 임베딩 요청
    await this.upsert(uuid(), vector, {
      text: dto.text,
      author: dto.authorId
    });
    return { ok: true };
  }
  
  async upsert(id: string, vector: number[], payload: Record<string, any>) {
    await this.client.upsert(this.collection, {
      wait: true,
      points: [{ id, vector, payload }],
    });
  }

검색

Qdrant에 저장해둔 문서를 검색해보자. payload에 넣어둔 키값이나, 유사도를 기준으로 필터링하는 것이 가능하다.

이번 예시는 유사한 문서를 찾을 벡터값을 통해 유사도가 특정 임계값을 넘는 문서 and payload의 특정 키 값을 만족하는 문서를 가져오는 코드이다.

검색할때는 QdrantClinet에서 제공하는 search 함수를 사용하면 된다.

  async searchTopVectorByMember(
    collection: string,
    vector: number[],
    memberId: string,
    threshold: number,
  ) {
    return this.client.search(collection, {
      vector,
      limit: 1,
      score_threshold: threshold,
      filter: {
        must: [
          {
            key: 'memberId',
            match: {
              value: memberId,
            },
          },
        ],
      },
    });
  }

 

인자가 4개나 있는데, 하나하나 천천히 뜯어보자

  • collection : 검색을 수행할 컬렉션
  • vector : 질의한 문서의 임베딩 벡터값
  • memberId : 문서의 payload에 미리 넣어둔 key값
  • threshold : 유사도가 이 값 이상 넘어야 검색되게 하겠다!! 하면 필요한 값

자 이제 search 함수를 사용한 것을 볼 수 있을텐데, limit는 검색된 문서들 중 몇 개만 보이게 할 건지를 넘겨주는 속성이고, score_threshold는 유사도 임계값을 넘겨주는 속성이다. filter는 RDB에 where 절에 해당하는 조건문이다.

여기서도 AND, NOT, OR 조건을 걸어줄 수 있는데, 예시는 다음과 같다

filter: {
  must: [...],        // AND 조건
  must_not: [...],    // NOT 조건
  should: [...],      // OR 조건
}

속성이 문자열 일 때, match를 통해 일치하는 문자열을 찾을 수 있다. 단, RDB의 contains나 %%처럼 포함 여부를 계산할 수는 없다.

속성이 숫자라면, 범위 검색이 가능하다. 지원되는 연산자는 아래와 같다

  • gt ( >)
  • get (>=)
  • lt (<)
  • lte (<=)
filter: {
  must: [
    {
      key: 'memberId',
      match: {
        value: 'abc123',
      },
    },
    {
      key: 'score',
      range: {
        gte: 0.8,
      },
    },
  ],
}

위의 예시는 payload의 memberId가 'abc123'이고, score가 0.8보다 큰 문서들을 검색하는 코드 예시다.

수정

이제 문서를 수정해보자. 문서에는 id, vector, payload가 있는데 vector와 payload 수정이 가능하다.

기본적으로 기존 문서의 id에 upsert를 진행하면 해당하는 id의 문서가 새 문서로 덮어씌워진다.

먼저 벡터를 업데이트하고 싶다면 updateVectors 함수를 사용하면 된다.

  public async updateVector(collection: string, id: string, vector: number[]) {
    await this.client.updateVectors(collection, {
      points: [{ id, vector }],
    });
  }

수정할 문서가 있는 컬렉션, 문서의 id, 그리고 새 벡터값을 넘겨받아서 사용하면 끝

그러면 페이로드만 수정하고 싶을때는 어떻게 하면 될까?

await this.client.setPayload('your_collection', {
  payload: {
    title: 'Only Title Changed',
    score: 0.85,
  },
  points: [123], // 해당 ID만
});

위 예시는 setPayload를 사용해 id가 123인 문서의 payload 내의 title과 score를 수정하는 코드이다.

만약 특정 필드를 지우고 싶다면?

await client.deletePayload('your_collection', {
  keys: ['title', 'score'], // 지울 필드
  points: [123],
});

이렇게 deletePayload 함수를 사용하면 되겠다.

삭제

먼저 delete 함수를 이용해서 문서를 삭제하는 코드를 보자. 컬렉션은 유지하면서 내부의 모든 문서를 지우고 싶을 떄는

await client.delete('your_collection', {
  filter: {}, // 빈 필터는 전체 삭제
});

이렇게 빈 필터를 넣어주면 되겠다.

조건을 만족하는 문서만 지울수도 있다.

await client.delete('your_collection', {
  points: [123], // id가 123인 문서만 삭제
});

이렇게 id기반으로 삭제하거나

await client.delete('your_collection', {
  filter: {
    must: [
      {
        key: 'memberId',
        match: {
          value: 'user-123',
        },
      },
    ],
  },
});

payload 내의 key값이 만족되는 문서만 지울수도 있다.

 

그렇다면 컬렉션 전체를 지울 때는 어떻게 할까?

이 때는 deleteCollection을 사용해서 컬렉션 자체를 지우면 된다.

await client.deleteCollection('your_collection');

또는 DELETE method로 {qdrant URL} / {collection-name}으로 rest-api를 요청해도 지워진다

 

나는 Qdrant를 RAG를 구현하면서 사용했다. 다음 포스팅은 RAG 구현기로 돌아오겠다.

'NestJS' 카테고리의 다른 글

RAG 구현기  (4) 2025.08.09
AWS bedrock 사용기  (5) 2025.08.05
반환 정보 편집하기  (1) 2025.06.15
입력 정보 검증하기  (0) 2025.06.15
TypeORM 간단 사용법  (0) 2025.06.15
정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차