이미 DeepSearch/DeepResearch 구현 가이드를 읽으셨다면, 품질을 크게 개선할 수 있는 몇 가지 세부 사항을 더 자세히 살펴보겠습니다. 이 포스트에서는 두 가지 주요 과제에 초점을 맞추겠습니다: 긴 웹페이지에서 스니펫을 선택하기 위한 임베딩 활용과 크롤링할 URL의 우선순위를 정하기 위한 리랭커 사용입니다.
이전에 우리가 "임베딩은 STS 태스크(semantic textual similarity)와 같은 쿼리 중복 제거에만 유용했고, 리랭커는 원래 DeepSearch 구현에 포함되지도 않았다"고 결론 내렸던 것을 기억하실 수 있습니다. 하지만 두 가지 모두 여전히 상당히 가치가 있는 것으로 밝혀졌습니다 - 단지 일반적으로 예상하는 방식과는 다르게 말이죠. 우리는 항상 가능한 한 가장 간단한 경로를 따랐습니다. 임베딩과 리랭커 제공자로서 우리의 가치를 정당화하기 위해 구성 요소를 추가하지는 않았습니다. 우리는 검색이 기본적으로 진정으로 필요로 하는 것에 기반을 두고 있습니다.
몇 주간의 실험과 반복을 거친 후, DeepSearch/DeepResearch 시스템에서 두 가지를 모두 활용할 수 있는 독특하면서도 효과적인 방법을 발견했습니다. 이를 적용함으로써 Jina DeepSearch의 품질을 크게 개선했습니다(직접 사용해보세요). 이 분야에서 일하는 동료들과 이러한 통찰을 공유하고자 합니다.
tag긴 콘텐츠에서 스니펫 선택하기
문제는 이렇습니다: Jina Reader를 사용하여 웹페이지 콘텐츠를 읽은 후, 이를 에이전트의 컨텍스트에 지식 항목으로 추가해야 합니다. 전체 콘텐츠를 LLM의 컨텍스트 윈도우에 덤프하는 것이 가장 간단한 방법이지만, 토큰 비용과 생성 속도를 고려할 때 최적의 방법은 아닙니다. 실제로는 콘텐츠 중 질문과 가장 관련 있는 부분을 식별하여 해당 부분만 선택적으로 에이전트의 컨텍스트에 지식으로 추가해야 합니다.
LLM 기반 필터링은 동일한 비용과 지연 시간 문제가 있으므로, 더 작은 모델의 솔루션을 찾아보겠습니다: 더 작고 저렴하면서도 다국어를 지원하는 모델이 필요합니다 - 쿼리나 문서가 항상 영어일 것이라고 보장할 수 없기 때문에 이는 매우 중요한 요소입니다.
한쪽에는 질문이 있고(원래 쿼리나 gap 질문), 다른 쪽에는 대부분의 내용이 관련 없는 긴 마크다운 콘텐츠가 있습니다. 우리는 쿼리와 가장 관련 있는 스니펫을 선택해야 합니다. 이는 2023년부터 RAG 커뮤니티가 다뤄온 청킹 문제와 비슷합니다 - 요약을 위한 컨텍스트 윈도우에 배치할 관련 청크만을 retriever 모델을 사용하여 검색하는 것입니다. 하지만 우리의 경우 두 가지 중요한 차이점이 있습니다:
- 제한된 문서에서 나온 제한된 청크. 각 청크가 약 500개의 토큰을 포함한다고 가정할 때, 일반적인 긴 웹 문서는 약 200,000 토큰(p50)에서 1,000,000 토큰(p99)을 포함하고, 각 단계에서 Jina Reader로 4-5개의 URL을 가져오면 수백 개의 청크가 생성됩니다 - 즉 수백 개의 임베딩 벡터와 수백 개의 코사인 유사도를 의미합니다. 이는 벡터 데이터베이스 없이 인메모리 JavaScript로 쉽게 관리할 수 있습니다.
- 효과적인 지식 스니펫을 형성하기 위해서는 연속적인 청크가 필요합니다.
[1-2, 6-7, 9, 14, 17, ...]와 같이 흩어진 문장들을 조합한 스니펫은 받아들일 수 없습니다. 더 유용한 지식 스니펫은[3-15, 17-24, ...]와 같은 패턴을 따라야 합니다 - 항상 연속적인 텍스트를 유지해야 합니다. 이렇게 하면 LLM이 지식 소스에서 복사하고 인용하기가 더 쉬워지고 환각을 줄일 수 있습니다.
나머지는 실무자들이 불평했던 모든 주의사항입니다: 임베딩 모델이 긴 컨텍스트를 잘 처리하지 못하기 때문에 각 청크가 너무 길 수 없고; 청킹은 컨텍스트 손실을 초래하고 청크 임베딩을 i.i.d로 만들며; 가독성과 의미론을 모두 유지하는 최적의 경계 신호를 어떻게 찾을 것인가? 이러한 문제들에 대해 알고 있다면, RAG 구현에서 이러한 문제들로 고민해보셨을 것입니다.
하지만 긴 이야기를 짧게 하면 - late-chunking과 jina-embeddings-v3가 이 세 가지 문제를 모두 아름답게 해결합니다. Late chunking은 각 청크의 컨텍스트 정보를 유지하고, 경계 신호에 둔감하며, jina-embeddings-v3 자체가 비대칭 다국어 검색 태스크에서 SOTA입니다. 자세한 내용은 우리의 블로그 포스트나 논문을 참고하시면 되지만, 여기 전반적인 구현을 소개합니다.
Conv1D와 유사하게 작동하는 스니펫 선택 알고리즘을 보여줍니다. 프로세스는 긴 문서를 고정 길이 청크로 분할하는 것으로 시작하여, late-chunking 토글이 켜진 jina-embeddings-v3로 임베딩합니다. 각 청크와 쿼리 간의 유사도 점수를 계산한 후, 슬라이딩 윈도우가 유사도 점수를 가로질러 이동하며 가장 높은 평균값을 가진 윈도우를 찾습니다.


function cherryPick(question, longContext, options) {
if (longContext.length < options.snippetLength * options.numSnippets)
return longContext;
const chunks = splitIntoChunks(longContext, options.chunkSize);
const chunkEmbeddings = getEmbeddings(chunks, "retrieval.passage");
const questionEmbedding = getEmbeddings([question], "retrieval.query")[0];
const similarities = chunkEmbeddings.map(embed =>
cosineSimilarity(questionEmbedding, embed));
const chunksPerSnippet = Math.ceil(options.snippetLength / options.chunkSize);
const snippets = [];
const similaritiesCopy = [...similarities];
for (let i = 0; i < options.numSnippets; i++) {
let bestStartIndex = 0;
let bestScore = -Infinity;
for (let j = 0; j <= similarities.length - chunksPerSnippet; j++) {
const windowScores = similaritiesCopy.slice(j, j + chunksPerSnippet);
const windowScore = average(windowScores);
if (windowScore > bestScore) {
bestScore = windowScore;
bestStartIndex = j;
}
}
const startIndex = bestStartIndex * options.chunkSize;
const endIndex = Math.min(startIndex + options.snippetLength, longContext.length);
snippets.push(longContext.substring(startIndex, endIndex));
for (let k = bestStartIndex; k < bestStartIndex + chunksPerSnippet; k++)
similaritiesCopy[k] = -Infinity;
}
return snippets.join("\n\n");
}질문에 대한 최적의 스니펫을 선택하기 위해 late chunking과 Conv1D와 유사한 평균 풀링을 사용합니다.
Jina Embeddings API를 호출할 때 retrieval task,late_chunking 및 truncate를 아래와 같이 설정하세요:
await axios.post(
'https://api.jina.ai/v1/embeddings',
{
model: "jina-embeddings-v3",
task: "retrieval.passage",
late_chunking: true,
input: chunks,
truncate: true
},
{ headers }); 질문 임베딩의 경우 task를 retrieval.query로 변경하고 late_chunking을 비활성화하세요
전체 구현은 Github에서 확인할 수 있습니다:
tag다음 읽을 URL 순위 매기기
문제는 다음과 같습니다: DeepSearch 세션 동안 검색 엔진 결과 페이지(SERP)에서 많은 URL을 수집하고 개별 웹페이지를 읽을 때마다 더 많은 URL을 발견하게 됩니다(페이지 내 링크들). 고유 URL의 총 개수는 쉽게 수백 개에 도달할 수 있습니다. 다시 말하지만, 모든 URL을 직접 LLM의 컨텍스트에 덤프하는 것은 비효율적입니다 - 귀중한 컨텍스트 윈도우 공간을 낭비하고 더 문제가 되는 것은, LLM이 본질적으로 무작위로 URL을 선택한다는 것을 발견했습니다. 필요한 답변을 포함할 가능성이 가장 높은 URL로 LLM을 안내하는 것이 중요합니다.
curl https://r.jina.ai/https://example.com \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Retain-Images: none" \
-H "X-Md-Link-Style: discarded" \
-H "X-Timeout: 20" \
-H "X-With-Links-Summary: all"DeepSearch에서 Jina Reader를 사용하여 페이지를 크롤링하는 최선의 옵션입니다. 이는 모든 페이지 내 링크를 별도의 links 필드에 수집하고, content 필드에서 제거합니다.
이 문제를 세션 중에 수백 개의 URL의 가중치를 매겨야 하는 컨텍스트 내 PageRank로 생각해보세요. 우리는 마지막 업데이트 시간, 도메인 빈도, 경로 구조, 그리고 가장 중요하게는 쿼리에 대한 의미적 관련성을 결합하여 복합 점수를 만들어 URL의 순위를 매깁니다. URL을 실제로 방문하기 전에 사용할 수 있는 정보만 사용할 수 있다는 것을 기억하세요:
빈도 신호: 여러 소스에서 여러 번 나타나는 URL은 추가 가중치를 받습니다. 검색 결과에서 자주 나타나는 도메인의 URL은 부스트를 받습니다. 인기 있는 도메인은 종종 권위 있는 콘텐츠를 포함하기 때문입니다.
경로 구조: URL 경로를 분석하여 콘텐츠 클러스터를 식별합니다. 공통 경로 계층 구조 내의 URL은 더 높은 점수를 받으며, 더 깊은 경로에는 감쇠 요소가 적용됩니다.
의미적 관련성: jina-reranker-v2-base-multilingual를 사용하여 질문과 각 URL의 텍스트 정보 간의 의미적 관련성을 평가합니다. 이는 전형적인 재순위화 문제입니다. 각 URL의 텍스트 정보는 다음에서 가져옵니다:
- SERP API 결과의 제목 및 스니펫 (
https://s.jina.ai/에'X-Respond-With': 'no-content'사용) - 페이지 내 URL의 앵커 텍스트 (
https://r.jina.ai에'X-With-Links-Summary': 'all'사용)
마지막 업데이트 시간: 일부 DeepSearch 쿼리는 시간에 민감하므로, 최근에 업데이트된 URL이 오래된 URL보다 더 가치가 있습니다. Google과 같은 주요 검색 엔진이 아닌 경우, 마지막 업데이트 시간을 신뢰성 있게 결정하는 것은 어렵습니다. 다음 신호를 결합하여 필요할 때 최신 콘텐츠를 우선시하는 신뢰도가 있는 타임스탬프를 제공하는 다층적 접근 방식을 구현했습니다.
- SERP API 필터 (s.jina.ai의 최신성 필터링을 위한
tbs파라미터와 같은) - HTTP 헤더 분석 (Last-Modified, ETag)
- 메타데이터 추출 (메타 태그, Schema.org 타임스탬프)
- 콘텐츠 패턴 인식 (HTML의 가시적 날짜)
- WordPress, Drupal, Ghost와 같은 플랫폼의 CMS 특정 지표
제한된 콘텐츠: 소셜 미디어 플랫폼의 일부 콘텐츠는 제한되어 있거나 단순히 유료 콘텐츠이며, 로그인하거나 ToS를 위반하지 않고는 이 콘텐츠에 접근할 정당한 방법이 없습니다. 접근할 수 없는 콘텐츠에 시간을 낭비하는 것을 방지하기 위해 문제가 있는 URL과 호스트 이름 목록을 적극적으로 유지하여 순위를 낮춰야 합니다.
도메인 다양성: 일부 경우에는 가장 높은 가중치의 URL이 모두 동일한 호스트 이름에서 나옵니다. 이는 DeepSearch를 지역 최적점에 가두고 결과의 최종 품질을 저하시킬 수 있습니다. 위의 예시에서 모든 상위 URL이 StackOverflow에서 나온 것을 확인하세요. 다양성을 개선하기 위해 각 호스트 이름에서 상위 k개의 높은 순위 URL을 선택하는 탐색-활용 접근 방식을 구현할 수 있습니다.
URL 순위 매기기의 전체 구현은 우리의 Github에서 찾을 수 있습니다.
<action-visit>
- Crawl and read full content from URLs, you can get the fulltext, last updated datetime etc of any URL.
- Must check URLs mentioned in <question> if any
- Choose and visit relevant URLs below for more knowledge. higher weight suggests more relevant:
<url-list>
+ weight: 0.20 "https://huggingface.co/docs/datasets/en/loading": "Load - Hugging FaceThis saves time because instead of waiting for the Dataset builder download to time out, Datasets will look directly in the cache. Set the environment ...Some datasets may have more than one version based on Git tags, branches, or commits. Use the revision parameter to specify the dataset version you want to load ..."
+ weight: 0.20 "https://huggingface.co/docs/datasets/en/index": "Datasets - Hugging Face🤗 Datasets is a library for easily accessing and sharing datasets for Audio, Computer Vision, and Natural Language Processing (NLP) tasks. Load a dataset in a ..."
+ weight: 0.17 "https://github.com/huggingface/datasets/issues/7175": "[FSTimeoutError] load_dataset · Issue #7175 · huggingface/datasetsWhen using load_dataset to load HuggingFaceM4/VQAv2, I am getting FSTimeoutError. Error TimeoutError: The above exception was the direct cause of the following ..."
+ weight: 0.15 "https://github.com/huggingface/datasets/issues/6465": "`load_dataset` uses out-of-date cache instead of re-downloading a ...When a dataset is updated on the hub, using load_dataset will load the locally cached dataset instead of re-downloading the updated dataset."
+ weight: 0.12 "https://stackoverflow.com/questions/76923802/hugging-face-http-request-on-data-from-parquet-format-when-the-only-way-to-get-i": "Hugging face HTTP request on data from parquet format when the ...I've had to get the data from their data viewer using the parquet option. But when I try to run it, there is some sort of HTTP error. I've tried downloading ..."
</url-list>
</action-visit>URL 가중치를 에이전트의 컨텍스트에 포함시키고 LLM이 가중치를 준수하도록 지시하는 것을 잊지 마세요.
tag결론
2025년 2월 2일 DeepSearch 시스템 출시 이후,우리는 품질을 크게 향상시킨 두 가지 구현 세부사항을 발견했습니다. 놀랍게도,두 가지 모두 다국어 임베딩과 리랭커를 "인-컨텍스트" 방식으로 활용합니다 - 이러한 모델들이 일반적으로 필요로 하는 기존의 사전 계산된 인덱스보다 훨씬 작은 규모로 작동합니다. 이것이 우리가 처음에 간과했던 이유를 설명합니다.
이는 검색 기술의 미래가 흥미롭게 양극화될 것임을 시사합니다. Kahneman의 이중 프로세스 이론과 유사한 프레임워크를 고려해보세요:
- Fast-think (grep,BM25,SQL): 최소한의 계산 요구사항으로 빠르고 규칙에 기반한 패턴 매칭.
- Slow-think (LLM): 상당한 계산이 필요한 깊은 맥락 이해를 통한 포괄적 추론.
- Mid-think (임베딩,리랭커): 중간 단계에 갇혀있나요? 단순 패턴 매칭에는 너무 "고급스럽고" 의미론적이지만 진정한 추론 능력은 부족합니다.
우리는 경량화되고 효율적인 SQL/BM25가 초기 콘텐츠 검색을 처리하고 이를 깊은 처리를 위해 강력한 LLM에 직접 전달하는 이분화된 아키텍처의 인기를 목격하고 있을 수 있습니다. 이러한 LLM들은 이전에 특수한 중간 수준 모델을 필요로 했던 의미론적 기능들을 점점 더 통합하고 있습니다. Mid-think 모델의 남은 역할은 필터링,중복 제거,그리고 완전한 추론이 비효율적일 수 있는 제한된 범위의 작업과 같은 특수한 인-컨텍스트 작업으로 이동하고 있습니다.
그럼에도 불구하고,중요한 스니펫을 선택하고 URL을 순위 매기는 것은 DeepSearch/DeepResearch 시스템 품질에 직접적인 영향을 미치는 기본 구성 요소로 남아 있습니다. 우리의 통찰이 여러분의 구현에도 개선을 가져오기를 바랍니다.
쿼리 확장은 계속해서 또 다른 중요한 품질 결정 요소입니다. 우리는 기본적인 프롬프트 기반 재작성부터 작은 언어 모델과 추론 기반 방법에 이르기까지 다양한 접근 방식을 적극적으로 평가하고 있습니다. 이에 대한 향후 연구 결과를 곧 확인하실 수 있습니다. 계속 지켜봐 주세요.











