Matryoshka 모델로 벡터 차원 줄이기

Matryoshka 모델로 벡터 차원 줄이기 (inference-matryoshka-models)

벡터 차원이 크면 저장 비용과 추론 지연이 커져요. Matryoshka Representation Learning(MRL) 기법을 쓰면 정보 손실을 최소화하면서 벡터 크기를 줄일 수 있는데, Qdrant Cloud에서 어떻게 활용하는지 살펴볼게요.

출처: Qdrant 공식문서

Matryoshka Representation Learning(MRL)은 임베딩 모델을 학습해 정보 손실을 최소화하면서 크기를 줄일 수 있는 벡터를 만들게 하는 기법이에요. Qdrant Cloud에서는 지원 모델에 대해 options 객체 안의 mrl 파라미터를 지정하면 벡터 크기를 원하는 차원으로 줄일 수 있어요.

Qdrant Cloud의 MRL은 같은 벡터의 여러 크기가 필요할 때 비용과 지연 시간을 줄이는 데 도움을 줘요. 각 벡터 크기마다 여러 번 추론 요청을 보내는 대신, 추론 서비스는 전체 크기 벡터에 대한 임베딩만 생성한 뒤 요청된 각 작은 크기로 벡터를 축소해요.

다음 예시는 원본 전체 크기 벡터(large)와 축소된 크기 벡터(small)를 모두 컬렉션에 삽입하는 방법을 보여줘요.

 PUT /collections/{collection_name}/points?wait=true { "points": [ { "id": 1, "vector": { "large": { "text": "Recipe for baking chocolate chip cookies", "model": "openai/text-embedding-3-small", "options": { "openai-api-key": "<YOUR_OPENAI_API_KEY>" } }, "small": { "text": "Recipe for baking chocolate chip cookies", "model": "openai/text-embedding-3-small", "options": { "openai-api-key": "<YOUR_OPENAI_API_KEY>", "mrl": 64 } } } } ] }
 from qdrant_client import QdrantClient , models from qdrant_client.context_headers import headers client = QdrantClient ( url = "https://xyz-example.qdrant.io:6333" , api_key = "<your-qdrant-api-key>" , cloud_inference = True ) with headers ({ "openai-api-key" : "<YOUR_OPENAI_API_KEY>" }): client . upsert ( collection_name = " {collection_name} " , points = [ models . PointStruct ( id = 1 , vector = { "large" : models . Document ( text = "Recipe for baking chocolate chip cookies" , model = "openai/text-embedding-3-small" , ), "small" : models . Document ( text = "Recipe for baking chocolate chip cookies" , model = "openai/text-embedding-3-small" , options = { "mrl" : 64 }, ) }, ) ], )
 import { QdrantClient , withHeaders } from "@qdrant/js-client-rest" ; await withHeaders ({ 'openai-api-key' : '<YOUR_OPENAI_API_KEY>' }, () => client . upsert ( "{collection_name}" , { points : [ { id : 1 , vector : { large : { text : 'Recipe for baking chocolate chip cookies' , model : 'openai/text-embedding-3-small' , }, small : { text : 'Recipe for baking chocolate chip cookies' , model : 'openai/text-embedding-3-small' , options : { mrl : 64 , }, }, }, }, ], }) );
 use std :: collections :: HashMap ; use qdrant_client :: { Payload , Qdrant , qdrant :: { Document , NamedVectors , PointStruct , UpsertPointsBuilder , Value }, }; client . with_header ( "openai-api-key" , "<YOUR_OPENAI_API_KEY>" ) . upsert_points ( UpsertPointsBuilder :: new ( "{collection_name}" , vec! [ PointStruct :: new ( 1 , NamedVectors :: default () . add_vector ( "large" , Document { text : "Recipe for baking chocolate chip cookies" . into (), model : "openai/text-embedding-3-small" . into (), options : HashMap :: new (), }, ) . add_vector ( "small" , Document { text : "Recipe for baking chocolate chip cookies" . into (), model : "openai/text-embedding-3-small" . into (), options : HashMap :: < String , Value > :: from_iter ( vec! [( "mrl" . into (), Value :: from ( 64 ), )]), }, ), Payload :: default (), )], ) . wait ( true ), ) . await ? ;
 import static io.qdrant.client.PointIdFactory.id ; import static io.qdrant.client.ValueFactory.value ; import static io.qdrant.client.VectorFactory.vector ; import static io.qdrant.client.VectorsFactory.namedVectors ; import io.grpc.Context ; import io.qdrant.client.QdrantClient ; import io.qdrant.client.QdrantGrpcClient ; import io.qdrant.client.RequestHeaders ; import io.qdrant.client.grpc.Points.Document ; import io.qdrant.client.grpc.Points.PointStruct ; import java.util.List ; import java.util.Map ; Context ctx = RequestHeaders . withHeader ( Context . current (), "openai-api-key" , "<YOUR_OPENAI_API_KEY>" ); ctx . call (() -> client . upsertAsync ( "{collection_name}" , List . of ( PointStruct . newBuilder () . setId ( id ( 1 )) . setVectors ( namedVectors ( Map . of ( "large" , vector ( Document . newBuilder () . setModel ( "openai/text-embedding-3-small" ) . setText ( "Recipe for baking chocolate chip cookies" ) . build ()), "small" , vector ( Document . newBuilder () . setModel ( "openai/text-embedding-3-small" ) . setText ( "Recipe for baking chocolate chip cookies" ) . putAllOptions ( Map . of ( "mrl" , value ( 64 ))) . build ())))) . build ())) . get ());
 using Qdrant.Client ; using Qdrant.Client.Grpc ; using ( RequestHeaders . Use ( "openai-api-key" , "<YOUR_OPENAI_API_KEY>" )) await client . UpsertAsync ( collectionName : "{collection_name}" , points : new List < PointStruct > { new () { Id = 1 , Vectors = new Dictionary < string , Vector > { ["large"] = new Document () { Model = "openai/text-embedding-3-small" , Text = "Recipe for baking chocolate chip cookies" , }, ["small"] = new Document () { Model = "openai/text-embedding-3-small" , Text = "Recipe for baking chocolate chip cookies" , Options = { [ "mrl" ] = 64 }, }, }, }, } );
 import ( "context" "github.com/qdrant/go-client/qdrant" ) ctx := qdrant . WithHeader ( context . Background (), "openai-api-key" , "<YOUR_OPENAI_API_KEY>" ) client . Upsert ( ctx , & qdrant . UpsertPoints { CollectionName : "{collection_name}" , Points : [] * qdrant . PointStruct { { Id : qdrant . NewIDNum ( uint64 ( 1 )), Vectors : qdrant . NewVectorsMap ( map [ string ] * qdrant . Vector { "large" : qdrant . NewVectorDocument ( & qdrant . Document { Model : "openai/text-embedding-3-small" , Text : "Recipe for baking chocolate chip cookies" , }), "small" : qdrant . NewVectorDocument ( & qdrant . Document { Model : "openai/text-embedding-3-small" , Text : "Recipe for baking chocolate chip cookies" , Options : qdrant . NewValueMap ( map [ string ] any { "mrl" : 64 , }), }), }), }, }, } )

한 가지 눈여겨볼 점은, 요청에 두 개의 추론 객체가 들어 있음에도 Qdrant Cloud의 추론 서비스는 OpenAI API에 추론 요청을 한 번만 보낸다는 거예요. 따라서 왕복이 하나 줄어들고 비용도 절감돼요.

MRL의 좋은 사용 사례는 작은 벡터로 프리페칭(prefetching)을 하고, 이어서 원본 크기 벡터로 재점수화(re-scoring)해서 속도와 정확성의 균형을 잡는 방식이에요. 아래 예시는 먼저 64차원 축소 벡터(small)로 1000개의 후보를 프리페치한 뒤, 원본 전체 크기 벡터(large)로 재점수화해 가장 관련성 높은 상위 10개를 반환해요.

 POST /collections/{collection_name}/points/query { "prefetch": { "query": { "text": "How to bake cookies?", "model": "openai/text-embedding-3-small", "options": { "openai-api-key": "<YOUR_OPENAI_API_KEY>", "mrl": 64 } }, "using": "small", "limit": 1000 }, "query": { "text": "How to bake cookies?", "model": "openai/text-embedding-3-small", "options": { "openai-api-key": "<YOUR_OPENAI_API_KEY>" } }, "using": "large", "limit": 10 }
 from qdrant_client import QdrantClient , models from qdrant_client.context_headers import headers client = QdrantClient ( url = "https://xyz-example.qdrant.io:6333" , api_key = "<your-qdrant-api-key>" , cloud_inference = True ) with headers ({ "openai-api-key" : "<YOUR_OPENAI_API_KEY>" }): client . query_points ( collection_name = " {collection_name} " , query = models . Document ( text = "How to bake cookies?" , model = "openai/text-embedding-3-small" , ), using = "large" , limit = 10 , prefetch = models . Prefetch ( query = models . Document ( text = "How to bake cookies?" , model = "openai/text-embedding-3-small" , options = { "mrl" : 64 }, ), using = "small" , limit = 1000 , ) )
 import { QdrantClient , withHeaders } from "@qdrant/js-client-rest" ; await withHeaders ({ 'openai-api-key' : '<YOUR_OPENAI_API_KEY>' }, () => client . query ( "{collection_name}" , { prefetch : { query : { text : "How to bake cookies?" , model : "openai/text-embedding-3-small" , options : { mrl : 64 , } }, using : 'small' , limit : 1000 , }, query : { text : "How to bake cookies?" , model : "openai/text-embedding-3-small" , }, using : 'large' , limit : 10 , }) );
 use std :: collections :: HashMap ; use qdrant_client :: { Qdrant , qdrant :: { Document , PrefetchQueryBuilder , Query , QueryPointsBuilder , Value }, }; client . with_header ( "openai-api-key" , "<YOUR_OPENAI_API_KEY>" ) . query ( QueryPointsBuilder :: new ( "{collection_name}" ) . add_prefetch ( PrefetchQueryBuilder :: default () . query ( Query :: new_nearest ( Document { text : "How to bake cookies?" . into (), model : "openai/text-embedding-3-small" . into (), options : HashMap :: < String , Value > :: from_iter ( vec! [( "mrl" . into (), Value :: from ( 64 ), )]), })) . using ( "small" ) . limit ( 1000_ u64 ), ) . query ( Query :: new_nearest ( Document { text : "How to bake cookies?" . into (), model : "openai/text-embedding-3-small" . into (), options : HashMap :: new (), })) . using ( "large" ) . limit ( 10_ u64 ) . build (), ) . await ? ;
 import static io.qdrant.client.QueryFactory.nearest ; import static io.qdrant.client.ValueFactory.value ; import io.grpc.Context ; import io.qdrant.client.QdrantClient ; import io.qdrant.client.QdrantGrpcClient ; import io.qdrant.client.RequestHeaders ; import io.qdrant.client.grpc.Points ; import io.qdrant.client.grpc.Points.Document ; import io.qdrant.client.grpc.Points.PrefetchQuery ; import java.util.Map ; Context ctx = RequestHeaders . withHeader ( Context . current (), "openai-api-key" , "<YOUR_OPENAI_API_KEY>" ); ctx . call (() -> client . queryAsync ( Points . QueryPoints . newBuilder () . setCollectionName ( "{collection_name}" ) . addPrefetch ( PrefetchQuery . newBuilder () . setQuery ( nearest ( Document . newBuilder () . setModel ( "openai/text-embedding-3-small" ) . setText ( "How to bake cookies?" ) . putAllOptions ( Map . of ( "mrl" , value ( 64 ))) . build ())) . setUsing ( "small" ) . setLimit ( 1000 ) . build ()) . setQuery ( nearest ( Document . newBuilder () . setModel ( "openai/text-embedding-3-small" ) . setText ( "How to bake cookies?" ) . build ())) . setUsing ( "large" ) . build ()) . get ());
 using Qdrant.Client ; using Qdrant.Client.Grpc ; using ( RequestHeaders . Use ( "openai-api-key" , "<YOUR_OPENAI_API_KEY>" )) await client . QueryAsync ( collectionName : "{collection_name}" , prefetch : [ new() { Query = new Document() { Model = "openai/text-embedding-3-small", Text = "How to bake cookies?", Options = { ["mrl"] = 64 }, }, Using = "small" , Limit = 1000 , }, ], query : new Document () { Model = "openai/text-embedding-3-small" , Text = "How to bake cookies?" , }, usingVector : "large" , limit : 10 );
 import ( "context" "github.com/qdrant/go-client/qdrant" ) ctx := qdrant . WithHeader ( context . Background (), "openai-api-key" , "<YOUR_OPENAI_API_KEY>" ) client . Query ( ctx , & qdrant . QueryPoints { CollectionName : "{collection_name}" , Prefetch : [] * qdrant . PrefetchQuery { { Query : qdrant . NewQueryNearest ( qdrant . NewVectorInputDocument ( & qdrant . Document { Model : "openai/text-embedding-3-small" , Text : "How to bake cookies?" , Options : qdrant . NewValueMap ( map [ string ] any { "mrl" : 64 , }), }), ), Using : qdrant . PtrOf ( "small" ), Limit : qdrant . PtrOf ( uint64 ( 1000 )), }, }, Query : qdrant . NewQueryNearest ( qdrant . NewVectorInputDocument ( & qdrant . Document { Model : "openai/text-embedding-3-small" , Text : "How to bake cookies?" , }), ), Using : qdrant . PtrOf ( "large" ), Limit : qdrant . PtrOf ( uint64 ( 10 )), } )

더 알아보기 (Learn more)