임베딩

먼저 단어의 정의입니다. 임베딩은 텍스트를 대신하는 숫자의 목록이며 텍스트의 의미를 포착하므로, 비슷한 의미의 텍스트는 비슷한 숫자로 변환됩니다. 이 장은 단어를 컴퓨터가 비교할 수 있는 것으로 변환하는 방법에 대한 것이며, 이것이 키워드 대신 의미에 맞는 검색을 가능하게 합니다.
텍스트를 벡터로
컴퓨터는 어떻게 "개"와 "강아지"가 관련이 있다는 것을 알고, "개"와 "스탠드"는 관련이 없다는 것을 알까요? 이 단어들은 중요한 공통 글자가 없습니다.
비결은 텍스트를 그 의미를 포착한 숫자로 변환하는 것이므로, 비슷한 의미는 비슷한 숫자가 됩니다. 그 숫자의 목록을 **임베딩**이라고 하며, 벡터라고도 합니다. 모델이 텍스트를 읽고 벡터를 출력합니다.
당신이 숫자들을 직접 해석할 필요는 없습니다. 중요한 것은 그것들 사이의 관계입니다.
대략적인 정신 이미지: 비슷한 의미의 텍스트들이 서로 가까이 있는 공간에 모든 텍스트가 점으로 배치되어 있다고 생각해보세요. "개"와 "강아지"는 같은 동네에 있고, "스탠드"는 반대편 도시에 있습니다. 실제 임베딩은 당신이 상상할 수 있는 2~3개 차원이 아니라 수백 개 또는 수천 개의 차원을 사용하지만, 직관은 유지됩니다: 가까움은 유사성을 의미합니다.
그 숫자들은 어디서 오고, 왜 의미와 일치할까요? 임베딩 모델은 LLM이 작동하는 방식의 예측 모델과 가까운 친척으로, 엄청난 양의 텍스트로 훈련됩니다. 자신의 작업을 잘하기 위해 어떤 단어와 개념이 비슷한 맥락에 나타나는지 학습해야 했는데, 이는 단어는 그것이 속한 회사로 알 수 있다는 오래된 관찰입니다.
"개"와 "강아지"는 같은 종류의 문장에 나타나므로 모델은 이들을 비슷하게 표현하도록 학습했습니다. "스탠드"는 다른 환경에 있으므로 다른 곳에 있습니다. 수백 개의 숫자는 의미의 학습된 방향으로, 각각 텍스트가 어떻게 다양한지의 축을 포착하고, 인간이 읽을 수 있는 방식으로 라벨이 붙지 않습니다. 숫자를 디코딩하지 않고 두 벡터가 얼마나 가까운지만 측정합니다.
의미 대신 철자에 일치하는 검색을 원하므로, "돈을 돌려받다"가 환불에 관한 줄을 찾습니다. 이동은 각 텍스트를 **벡터**로 변환하는 것입니다. 벡터는 고정 길이의 숫자 목록이며, 비슷한 의미가 가까이 있습니다. 임베딩 모델이 텍스트를 읽고 그 벡터를 내보내며, 핵심은 벡터 사이의 거리가 의미의 유사성을 나타낸다는 것입니다.
벡터는 설정된 길이인 차원을 가지며, 종종 수백 개에서 수천 개의 숫자입니다. 같은 모델로 임베딩한 모든 텍스트는 같은 길이로 나오며, 이것이 임의의 두 개를 비교할 수 있게 합니다. 그 차원은 의미의 학습된 방향이며, 인간이 라벨을 붙인 카테고리가 아니므로 차원 47을 "공식성"으로 읽지 않습니다. 벡터를 하나의 불투명한 점으로 취급하고 거리를 측정합니다.
실제로 뭔가를 임베딩하기 전에, 전체가 작동하는지를 결정하는 단계가 있습니다: 청킹입니다. 전체 문서를 하나의 벡터로 임베딩하는 경우는 드뭅니다. 20쪽 설명서 하나의 벡터는 모든 주제를 하나의 평균 점으로 흐릿하게 하여 거의 아무것도 잘 매칭되지 않기 때문입니다. 대신 문서를 청크, 대략 문단에서 페이지 정도의 구절로 나누고, 각 청크를 따로 임베딩합니다. 전체 문서가 아닌 청크를 임베딩하므로 각 벡터가 하나의 초점이 맞춘 아이디어를 전달합니다.
청크 크기는 의도적으로 설정하는 실제 트레이드오프입니다. 너무 큰 청크는 여러 아이디어를 평균화하고 느슨하게 검색합니다. 너무 작은 청크는 문장을 의미 있게 만든 주변 맥락을 잃습니다. 일반적인 시작점은 청크당 수백 개의 토큰에 이웃 간의 작은 겹침으로, 경계를 가로질러 나뉜 문장이 여전히 그들 중 하나에서 전체로 나타나도록 한 다음, 검색 결과에 따라 조정합니다.
의미에 맞는 검색은 하나의 아이디어에 달려 있습니다: 텍스트를 벡터 공간(각 점이 숫자의 목록인 고정 차원 좌표 공간)에 매핑하여 의미론적 유사성이 기하학적 근접성이 되도록 합니다. 임베딩 모델은 매핑입니다: 텍스트를 읽고 벡터를 반환합니다.
그 위에 구축하는 모든 것, 인덱싱, 검색, 순위 매김은 그 벡터에 대한 기하학입니다. 매핑을 잘못하면 다운스트림은 복구하지 못합니다.
첫 번째 설계 레버는 임베딩할 내용이며, 그것이 청킹입니다: 전체 파일을 임베딩하기보다는 임베딩 전에 소스 문서를 구절로 분할합니다. 이유는 임베딩이 입력을 하나의 점으로 손실 압축하므로, 청크에 포함된 서로 다른 아이디어가 많을수록, 모두 흐릿한 중심으로 평균화되어 날카롭게 검색되지 않기 때문입니다. 청크를 사용자가 질문할 단위인 대략 수백 개의 토큰이 되는 구절로 오른쪽 크기로 만들고, 경계를 걸쳐 있는 사실이 적어도 하나의 청크에서 생존하도록 작은 겹침을 가집니다.
청킹 전략은 모델 선택보다 검색 품질의 승패가 결정되는 곳입니다:
- 고정 크기 윈도우는 저렴하지만 중간에 문장을 끊습니다.
- 구조 인식 분할(제목, 문단 또는 코드 블록에서)은 각 청크를 일관되게 유지하고 더 잘 검색하는 경향이 있지만, 더 복잡한 파이프라인의 비용입니다.
선택한 것이 무엇이든, 청크의 소스와 위치를 벡터 옆에 저장하세요. 검색 시간에 사실이 어디서 왔는지 다시 가리켜야 하기 때문입니다. 그리고 벗겨진 벡터는 당신에게 말할 수 없습니다.
한 가지 더 내재화할 속성: 임베딩 차원은 해석 가능한 축이 아니므로, 벡터를 읽어서 검색을 디버깅할 수 없습니다. 임베딩을 검색한 것을 검사하여 디버깅하고, 절대 숫자를 읽어서 하지 마세요. 쿼리가 잘못된 청크를 당기면, 수정은 청킹, 모델 또는 쿼리에 있으며, 좌표 자체가 아닌 검색된 결과를 통해 검사합니다.
임베딩 생성
임베딩 모델에 벡터를 요청하며, 일반 모델 호출과 유사하지만 다른 엔드포인트와 다른 모델입니다.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small" # 임베딩 모델을 위해 교환 지점
def embed(text):
response = client.embeddings.create(model=EMBED_MODEL, input=text)
return response.data[0].embedding # 숫자의 목록
vector = embed("작고 장난기 많은 개")
print(len(vector)) # 예: 1536개의 숫자결과는 숫자의 목록입니다. 하나의 규칙이 나머지 위에 중요합니다: 항상 **같은 모델**로 만든 임베딩을 비교하세요. 다른 모델의 벡터는 다른 공간에 있고 비교할 수 없으며, 다른 단위의 측정처럼입니다. 모델을 섞으면 매번 무의미한 점수가 나옵니다.
그리고 모든 모델 호출처럼, 토큰으로 청구되므로, 큰 문서 컬렉션을 임베딩하는 것은 수천 개의 문서에 실행하기 전에 미리 추정할 가치가 있는 비용입니다.
호출 자체는 다른 엔드포인트이며 채팅 호출과 같은 형태입니다: 텍스트를 보내고, 벡터를 돌려받습니다.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small"
def embed_batch(texts):
# 많은 텍스트를 한 요청으로 보내고, 텍스트당 하나의 벡터를 돌려받습니다
response = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [item.embedding for item in response.data]
chunks = ["작고 장난기 많은 개", "어린 개", "사무용품"]
vectors = embed_batch(chunks) # 청크당 하나의 벡터, 입력과 같은 순서형태는 제공자에 따라 다르지만, 요청-응답 아이디어는 모든 곳에서 동일합니다. 기본 호출 이상의 두 가지 실제 포인트입니다. 첫째, **배치**로 임베딩하세요: 대부분 엔드포인트는 단일 요청에서 텍스트 목록을 가져가고 각각 하나의 벡터를 반환하며, 이는 청크당 하나의 호출보다 빠르고 저렴합니다. 요청 오버헤드를 덜 지불하고 속도 제한에 덜 닿기 때문입니다. 큰 코퍼스를 인덱싱할 때 배치는 비용과 벽시계 시간을 합리적으로 유지합니다.
두 번째, 같은 모델 규칙은 동반자를 가집니다: 모델과 차원은 기본값이 아닌 선택입니다. 더 많은 차원을 가진 더 큰 임베딩 모델은 더 미세한 차이를 포착하지만 계산, 저장 및 비교에 더 많은 비용이 듭니다. 당신의 데이터를 여전히 분리하는 가장 작은 모델과 차원을 선택하세요. 어떤 모델은 더 짧은 벡터를 요청하도록 허락하여 약간의 정확성을 저장 및 검색 속도의 저렴함으로 교환합니다. 무엇이든 선택하면 계약이 됩니다: 나중에 모델을 변경하면 저장된 모든 벡터를 다시 생성해야 합니다.
API 표면은 작습니다. 텍스트를 보내고 float 벡터를 받으며, 흥미로운 결정은 호출 자체가 아닌 그 주변에 있습니다.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small"
def embed_batch(texts):
# 하나의 요청, 많은 벡터 돌려받기, 입력 순서에서
response = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [item.embedding for item in response.data]요청 및 응답 형태는 제공자에 따라 다르지만, 모든 임베딩 엔드포인트는 이 동일한 배치-입력, 벡터-출력 계약입니다. 배치는 규모에서 좋은 것이 아니며, 인덱스 작업을 완료하는 것과 하루 종일 스로틀하는 것 사이의 차이입니다: 각 요청은 고정 오버헤드를 가지고 속도 제한에 대해 계산되므로, 큰 배치를 보내 둘 다 상환하고, 큰 작업이 닿을 부분 실패에 대해 물러서 재시도합니다.
모델과 차원은 긴 꼬리를 가진 건축 계약입니다. 더 높은 차원 벡터는 더 많은 구별을 저장하지만 생산에서 중요한 모든 축에 더 많이 드는 비용: 벡터당 저장소, 인덱스의 메모리, 그리고 비교당 시간, 모두 차원과 함께 확장됩니다. 어떤 모델은 마트료시카 임베딩(더 짧은 길이로 잘라내고 여전히 사용할 수 있는 벡터)을 지원하며, 이는 정확성을 위해 전체 벡터를 유지하고 저렴한 첫 번째 통과 검색을 위해 잘린 접두사를 제공할 수 있게 합니다. 코퍼스가 모델 품질이 아닌 저장소와 쿼리 지연이 제약하는 정도로 충분히 클 때 가치 있습니다.
나중에 물리는 계약은 버전입니다. 저장된 임베딩은 하나의 정확한 모델 버전에 영구적으로 연결됩니다.
두 모델 버전의 벡터는 비교할 수 없으므로 새 모델로 새 쿼리를 임베딩하고 이전 모델로 만든 인덱스에 대해 비교할 수 없습니다. 결과는 무음입니다: 점수는 타당해 보이고 순위는 틀립니다. 모델 버전을 인덱스의 스키마의 일부로 취급하고, 벡터 옆에 저장하고, 이 장의 뒷부분 신선도 논의에서 다룰 다시 임베딩을 마이그레이션으로 계획하세요.
유사성 측정
두 텍스트 조각이 얼마나 관련이 있는지 찾으려면, 벡터가 얼마나 가까운지 측정합니다. 표준 측정은 **코사인 유사성**입니다. 이는 -1에서 1까지의 숫자를 반환합니다: 1에 가까우면 더 유사하고, 0 근처면 무관합니다. 사용하기 위해 그 뒤의 수학을 알 필요가 없습니다. 두 벡터가 가리키는 방향을 비교하는 것으로 생각하세요.
방향이 여기서 의미를 전달하며, 코사인이 원시 거리 대신 각도를 비교하는 이유는 이것입니다. 환불에 대한 두 텍스트는 하나가 짧은 구절이든 긴 문단이든 같은 방향을 가리킵니다.
import math
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
return dot / (mag_a * mag_b)
dog = embed("작고 장난기 많은 개")
puppy = embed("어린 개")
stapler = embed("종이를 묶기 위한 사무용품")
print(cosine_similarity(dog, puppy)) # 더 높음, 둘은 의미에서 가깝습니다
print(cosine_similarity(dog, stapler)) # 더 낮음, 둘은 무관합니다숫자는 직관을 확인합니다: 개와 강아지는 더 높은 점수이고, 개와 스탠드는 더 낮은 점수입니다. 쿼리의 벡터를 저장된 벡터에 대해 비교하는 것은 의미론적 검색의 핵심입니다. 그 하나의 함수는 이 장의 나머지가 구축하는 이동입니다.
벡터가 있으면, 이제 두 개가 얼마나 가까운지 말하는 숫자가 필요합니다. 표준은 **코사인 유사성**입니다: 두 벡터 사이의 각도를 측정하고 길이를 무시하며 -1에서 1까지의 값을 반환하며 더 높으면 더 비슷합니다. 각도이지 거리가 아닌데, 같은 주제의 긴 구절과 짧은 구절이 벡터가 다른 크기를 가져도 유사하게 점수를 받기 때문입니다.
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
# 벡터가 길이 1로 정규화되면, 코사인 유사성은 그들의 점곱과 같습니다
score = dot(query_vector, doc_vector) # 나눗셈이 필요하지 않습니다규모에서 보상하는 이동은 여기에 있습니다: 저장할 때 **정규화**하여 길이 1로 벡터를 정규화하세요. 모든 벡터의 길이가 같으면, 코사인 유사성의 나눗셈이 사라지고 점수는 순수 점곱이 되며, 이는 차원당 하나의 곱하기-더하기이고 큰 집합에서 실행할 때 훨씬 빠릅니다. 저장 시 한 번 정규화하고, 점곱으로 비교하세요. 많은 임베딩 모델이 이미 정규화된 벡터를 반환하지만, 가정하지 마세요. 확인하세요.
장난감 예제에서 다른 변화: 단 하나의 가장 가까운 경기를 원하는 경우는 드뭅니다. top-k 검색을 원하며, k는 가장 높은 점수를 받는 결과입니다. 보통 3에서 10개입니다. 하나의 벡터는 거의 놓친 것일 수 있으며 두 번째와 세 번째가 함께 답을 가지고 있으며, 작은 집합을 반환하면 다음 단계에 작업 공간을 제공합니다. 오직 상위 하나만 반환하는 것은 당신이 유지하고 싶었던 맥락을 던지는 것입니다.
유사성 메트릭이 필요하며 코사인 유사성(두 벡터 사이의 각도의 코사인, -1에서 1까지)은 크기를 무시하기 때문이 기본입니다. 따라서 문서 길이가 주제를 압도하지 않습니다. 그 선택은 당신이 그 주변에 설계할 가치가 있는 성능 결과를 가집니다: 원시 벡터에 대한 코사인은 점곱에 두 크기 계산을 더하며, 인덱스의 모든 벡터에 대해 모든 쿼리에서 지불합니다.
import numpy as np
# 쓰기 시간에 정규화하며, 벡터당 한 번
def normalize(v):
v = np.asarray(v, dtype=np.float32)
return v / np.linalg.norm(v)
# 쿼리 시간에, 정규화된 벡터에 대한 코사인은 단일 행렬-벡터 곱입니다
def search(query_vec, matrix):
return matrix @ query_vec # 행당 하나의 점수, 벡터화됨최적화는 **정규화**를 위해 쓰기 시간에 모든 벡터를 단위 길이로 하는 것이며, 이는 코사인 유사성을 점곱과 동일하게 만들고 Python 루프 대신 단일 행렬-벡터 곱으로 전체 코퍼스를 점수 지을 수 있게 합니다. 쓰기 시간에 정규화하고 유사성은 전체 인덱스에 대해 한 번의 점곱입니다. 이것은 무차별 강제 기반선입니다: 정확하고, 직접적이고, 코퍼스 크기에 선형이며, 대략 낮은 수십만 벡터까지 괜찮으며 그 위에서 넘어지며, 이것이 나중에 다룬 근사 인덱스로 이동하는 신호입니다.
생산 현실이 장난감 메트릭을 숨기는 두 가지입니다. 첫째, argmax를 거의 원하지 않으며, 상위 결과와 두 번째 결과 사이의 갭이 경기가 얼마나 확실한지 말하고, 여전히 낮은 점수를 받는 상위 결과는 코퍼스의 아무것도 실제로 쿼리에 답하지 않음을 의미하기 때문에 점수 임계값을 가진 top-k를 원합니다. 그것을 "좋은 경기 없음"으로 반환하십시오. 그보다는 최악의 행을 돌려주십시오. 둘째, 코사인 유사성은 상대적이며 절대적이지 않습니다: 0.4의 점수는 당신의 코퍼스와 모델이 생산하는 점수의 분포에 대해서만 의미가 있으므로, 임계값을 블로그 게시물에서 복사하는 대신 인덱스당 경험적으로 보정하세요.
의미론적 검색
조각들을 함께 배치하면 키워드 대신 의미로 검색할 수 있습니다. 문서를 한 번 임베딩하고 벡터를 저장하세요. 쿼리가 들어오면, 이를 임베딩하고 벡터가 가장 가까운 문서를 반환합니다.
docs = [
"계정 설정 페이지에서 비밀번호를 재설정하세요.",
"환불 정책은 30일 이내에 반품을 허용합니다.",
"청구 질문은 이메일로 지원팀에 문의하세요.",
]
# 매 문서를 한 번 임베딩하고, 미리 저장하세요
index = [{"text": text, "vector": embed(text)} for text in docs]
def search(query):
query_vector = embed(query)
scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
return sorted(scored, key=lambda x: x["score"], reverse=True)[0] # 가장 가까운 일치
print(search("내 돈을 어떻게 돌려받나요?"))
# 쿼리와 공유된 중요한 단어가 없음에도 불구하고 환불 정책 줄을 반환합니다쿼리 "내 돈을 어떻게 돌려받나요?"는 이와 공유된 중요한 단어가 없음에도 불구하고 환불 줄을 찾습니다. 의미론적 검색은 철자가 아닌 의미에 맞습니다. 키워드 검색은 쿼리와 문서가 중요한 공통 단어가 없기 때문에 완전히 놓쳤을 것입니다.
형태는 항상 같습니다: 한 번 임베딩하고 벡터를 유지하고, 쿼리를 임베딩하고, 점수를 매기고, 정렬하고, 가장 가까운 것을 반환합니다. 그것이 의미론적 검색 루프이며, 당신은 이 핸드북의 나머지에서 이를 재사용할 것입니다.
장난감 버전은 Python 목록에 벡터를 저장하고 각 쿼리에 모든 벡터를 점수 매깁니다. 이것은 데모에서 작동하고 코퍼스가 자라는 순간 중단됩니다. 선형 스캔이 모든 쿼리에 대해 모든 벡터에 대해 전체 비교를 재실행하기 때문입니다. 또한 모든 재시작에서 메모리에 인덱스를 재구축합니다.
수정은 **벡터 데이터베이스**입니다: 벡터를 보유하고 빠르게 "이 쿼리 벡터에 가장 가까운 k개 찾기"라고 답하도록 구축된 저장소입니다. 당신의 목록과 루프 대신, 벡터를 한 번 작성하면 데이터베이스가 검색, 지속성 및 개별 항목 업데이트를 처리합니다.
# 벡터 DB에 따라 형태가 다르지만, 패턴은 모든 곳에서 같습니다
index.upsert([
{"id": "doc-1", "vector": embed(chunk), "metadata": {"source": "faq.md", "section": "환불"}}
for chunk in chunks
])
results = index.query(vector=embed(user_question), top_k=5) # 가장 가까운 5개, 단 1개만이 아닙니다데이터베이스가 목록에 제공하는 두 가지:
- 그것은 top-k 결과를 한 호출로 반환합니다. 가장 가까운 여러 개이며, 단 하나의 가장 좋은 것이 아니며, 이것이 실제로 다음 단계에 공급하는 것입니다.
- 그것은 메타데이터를 각 벡터 옆에 저장합니다. 소스 파일, 섹션 및 타임스탐프이므로 결과가 어디서 왔는지 다시 가리킬 수 있으며 필터링할 수 있습니다.
벡터를 벡터 DB에 저장하세요. Python 목록이 아니라, 실제 데이터를 가진 순간. 목록은 메커니즘을 배우기 위해 있습니다. 데이터베이스는 그것을 실행합니다.
참조 루프는 한 번 임베딩하고, 저장하고, 쿼리를 임베딩하고, 점수를 매기고, 정렬하고, 반환하는 것으로 정확하고, 규모와 접촉에서 생존하지 않습니다. 무차별 강제 스캔은 코퍼스 크기에 대해 선형이므로, 백만 벡터와 어떤 실제 쿼리 속도에서, 당신은 없는 CPU-초당 검색에 지출하고 있습니다. 생산 검색의 모든 것은 모든 벡터에 대해 쿼리를 비교하지 않습니다.
그것이 근사 최근접 이웃 검색이 당신에게 사는 것입니다. (ANN: 진정한 가장 가까운 것을 놓칠 작은 기회를 받아들여서 빠르게 가장 가까운 벡터를 찾는 알고리즘). 두 가족이 지배합니다:
- HNSW (계층적 네비게이트 가능한 작은 세계)는 벡터의 계층화된 그래프를 구축하고 거칠음에서 미세함까지 걸으며, 높은 메모리의 비용으로 낮은 쿼리 지연을 제공합니다.
- IVF (반전된 파일)은 벡터를 클러스터하고 가장 가까운 클러스터만 검색하며, 메모리를 적게 사용하지만 데이터가 변할 때 클러스터 중심을 재훈련할 필요가 있습니다.
몇 십만 벡터 위에서, ANN은 선택사항이 아니며, 인덱스입니다. 당신이 튜닝하는 트레이드오프는 회수 (인덱스가 실제로 반환하는 진정한 최근접 이웃의 일부)는 지연과 메모리에 대해: 회수를 1.0으로 밀고 쿼리는 느려지고, 이완하고 조용히 관련 청크를 놓칩니다.
장난감 루프가 완전히 숨기는 실패 모드는 신선도입니다. 인덱스는 스냅샷이므로, 소스 문서가 변경될 때, 저장된 벡터는 이제 낡은 것입니다. 그리고 계속해서 현재인 것처럼 검색됩니다. 문서 변경에 핵심을 두고 re-embedding 파이프라인이 필요합니다: 편집을 탐지하고, 그 문서를 다시 청킹하고 재임베딩하고, 새 벡터를 업서트하면서 이전 벡터를 삭제하고, 이상적으로 콘텐츠 해시로 추적되므로 모든 쿼리에 대해 전체 코퍼스가 아니라 실제로 변경된 것만 다시 임베딩하면 됩니다. 낡은 벡터는 검색 시스템이 거짓말을 시작하는 조용한 방법입니다.
마지막 격차는 정확한 경기입니다. 벡터 검색은 그것을 놓치도록 구축되어 있습니다: A-10293 주문 또는 오류 코드에 대한 쿼리는 퍼지 영역에 임베딩되고 그 문자열을 포함하는 하나의 문서를 표면화하지 않을 수 있습니다. 하이브리드 검색은 키워드 검색 (정확한 토큰을 못 박음)과 벡터 검색 (의미를 못 박음)을 함께 실행하고 순위를 병합하므로 식별자, 이름 및 드문 단어는 의미론적 흐림으로 손실되지 않습니다. 사용자가 주문 번호와 같은 정확한 문자열을 입력할 때마다 이를 도달하세요.
임베딩이 아닌 것
임베딩은 유사성 도구이지, 생각하는 도구가 아닙니다. 올바른 도구에 도달하도록 기억할 한계 몇 가지:
- 그들은 정확한 경기를 하지 않습니다. 정확한 주문 번호 또는 오류 코드를 찾으려면 순수 키워드 또는 데이터베이스 검색이 더 낫습니다.
- 그들은 추론하지 않습니다. 그들은 두 가지가 관련이 있다고 말하지만 왜인지, 그리고 하나가 다른 것에서 논리적으로 따르는지는 아닙니다.
- 그들은 낡습니다. 문서가 변경되면, 다시 임베딩하거나, 검색이 이전 콘텐츠를 가리킵니다.
임베딩은 관련된 것을 찾으며, 참 또는 최신인 것을 판단하지 않습니다. 각 갭은 더 나은 도구를 가지고 있으며, 어떤 것을 사용할지 아는 것이 기술의 절반입니다.
이 정도로 잘 작동하는 도구로 모든 곳에 도달하는 함정은 그것입니다. 세 가지 한계가 멈출 곳과 다른 것을 선택할 곳을 표시합니다.
정확한 경기가 첫 번째입니다. 임베딩은 주문 ID 또는 오류 코드를 다른 모든 것과 같은 퍼지 의미론적 공간으로 매핑하므로, A-10293은 정확히 자신에게 도착하지 않고 다른 주문 모양의 문자열 근처에 내립니다. 식별자, 코드 및 사용자가 문자 그대로 입력하는 값의 경우, 키워드 또는 데이터베이스 검색이 올바른 도구이며, 하이브리드 검색 (키워드와 벡터 검색이 함께 실행됨)은 동시에 둘 다 얻는 방법입니다.
추론이 두 번째입니다. 유사성은 추론이 아닙니다: 두 청크가 높은 점수는 그들이 관련된 것에 대해 있다는 뜻이고, 하나가 질문에 답하거나 논리적으로 다른 것에서 따르는 것이 아닙니다. 추론은 검색에서 일어나지 않으며, 그것에 검색된 청크를 공급하는 모델에서 일어납니다. 검색은 후보를 찾고, 모델이 생각합니다.
신선도가 세 번째이며 조용히 물리는 것입니다. 인덱스는 임베딩한 때의 스냅샷이므로, 편집된 문서는 다시 임베딩될 때까지 이전 단어를 반환하게 유지됩니다. 다시 임베딩을 변경할 때 시스템의 일부로 취급하세요. 누군가가 답이 잘못되었다는 것을 알아챌 때까지 손으로 하는 것이 아닙니다.
임베딩이 멈추는 것을 아는 것이 검색 시스템을 신뢰할 수 있게 유지하는 것이며, 생산에서 세 경계가 중요합니다.
정확한 경기는 튜닝 문제가 아닌 구조적 맹점입니다. 임베딩은 입력을 의미론적 인근으로 압축하므로, 식별자, 코드, SKU 및 드문 고유 명사는 자신에게 정확히 도착하지 않고 유사해 보이는 문자열 방향으로 퍼집니다. 순수 벡터 검색은 정확한 히트 위의 거의 놓친 것을 순위 매깁니다. 수정은 어휘 인덱스 (키워드 또는 BM25, 고전적인 단어-빈도 순위)와 벡터 인덱스를 융합하는 하이브리드 검색이므로, 정확한 토큰이 정확하게 경기하고 의미가 의미론적으로 경기하며, 주문 조회를 기하학에 도박하지 않습니다.
드리프트는 조용히 침식되는 경계입니다. **임베딩 드리프트**는 인덱스 뒤의 모델 버전이 변경될 때 당신이 얻는 불일치입니다: 두 버전의 벡터는 다른 공간을 점령하고 비교할 수 없으므로, 새 모델로 임베딩된 쿼리는 이전 쿼리로 구축된 인덱스에 대해 그럴듯해 보이는 무의미를 반환합니다. 인덱스 스키마에서 모델 버전을 핸들링하고, 모든 모델 업그레이드를 드롭인이 아닌 전체 다시 임베딩 마이그레이션으로 취급하세요. 오류가 없습니다. 조용히 더 나쁜 결과일 뿐입니다.
평가는 검색이 이미 실패할 때까지 사람들이 건너뛰는 경계입니다. 검색이 좋은지를 눈으로 판단할 수 없습니다. 당신은 그것을 측정합니다: 알려진 관련 청크를 가진 쿼리 집합을 유지하고 recall@k (올바른 청크가 상위 k에 도착했는가) 그리고 정밀도 (당신이 반환한 것이 관련되었는가)를 점수 매깁니다. recall@k를 측정하지 않으면, 검색이 작동하는지 알 수 없습니다.
평가 집합은 또한 새로운 청킹 전략, 모델 또는 하이브리드 가중치가 실제로 도움이 되었는지 알려주는 것입니다. 당신이 아직 알아챈 것이 아닌 한 실패 집합을 교환하지 않습니다. 검색 품질은 RAG이 서 있는 기초이므로, 느낌 확인이 아닌 실제 테스트를 얻습니다.
실제로
전체 흐름은 위의 search 함수에 있습니다: 작은 FAQ 항목 집합을 한 번 임베딩한 후, 의미로 쿼리에 답합니다. 패턴은 크기에 상관없이 동일합니다. 저장소만 변경됩니다.
다음 장에서 당신은 가장 가까운 경기를 모델에 공급하므로, 메모리 대신 문서에서 답합니다. 그 기법은 RAG이며, 당신이 여기서 쓴 것 위에 직접 구축되어 있습니다.
메커니즘은 정해져 있습니다: 청크, 배치 임베딩, 벡터 DB에 저장, 유사성으로 top-k 검색. 생산으로 갈 때 변경되는 것은 주변 배관입니다. 변경할 때 다시 임베딩하는 것, 필터링할 메타데이터, 핸들링할 모델 버전입니다.
검색된 청크가 끝이 아닙니다. 다음 장에서 당신은 문맥으로 모델에 공급하므로 동결된 훈련 대신 문서에서 답합니다. 그것이 RAG이고, 답의 품질은 당신이 여기서 구축한 검색으로 제한됩니다.
여기서 모든 것, 청킹, 정규화, ANN 인덱싱, 하이브리드 검색, 신선도, 그리고 recall@k 평가는 검색 계층이며, AI 시스템의 답이 좋은지를 조용히 결정하는 부분입니다. 모델은 검색이 그것에 넘겨주는 것 위에서만 추론할 수 있으므로, 자신감 있는 잘못된 답은 보통 생성 의상을 입은 검색 실패입니다.
다음 장으로의 다리입니다. RAG는 기초 문맥을 모델에 공급하는 이 검색 계층이며, 여기의 모든 실패 모드, 낡은 벡터, 놓친 정확한 경기, 낮은 회수는 거기서 잘못된 답으로 표면화됩니다. 검색이 측정되고 신선하도록 구축하세요. RAG는 서 있을 무언가 견고한 것을 가집니다.

