동의어 검색에서도 사용자가 입력한 단어를 먼저 찾기
문서 코드를 정확히 입력했는데, 그 코드가 없는 안내문이 먼저 나온다고 하자. 검색 사전이 붙인 동의어가 안내문에 더 많이 등장했기 때문이다. 찾을 수 있는 표현은 늘었지만 사용자가 직접 지정한 단어의 우선순위는 낮아진 셈이다.
DeepConnector의 파일 검색은 사용자 입력과 사전 확장어를 함께 검색하되 점수는 구분한다. 직접 입력한 단어의 일치에는 큰 가중치를 주고, 사전이 보탠 별칭의 일치에는 작은 가중치를 준다. 동의어로 검색 범위를 넓히면서도 원래 검색어를 우선하려는 설계다.
첫 상황은 설계를 설명하기 위한 가정이며 실제 고객 장애가 아니다. 아래 문서와 검색어도 설명용으로 만들었다. 검증에는 특정 구현 스냅샷에서 추출한 실제 스니펫 조립 코드를 사용했고, 검색 서버나 사전 서비스 전체를 실행한 것은 아니다.
검색할 단어와 점수를 매길 단어의 구분
설명용 문서 코드 SETUP01에 사전이 설치 안내, 설치 설명서, 셋업 가이드를 연결한다고 가정했다. 코드만 검색하면 코드를 적지 않은 안내문은 찾지 못할 수 있다. 별칭도 함께 검색하면 그런 문서를 찾을 여지가 생긴다.
문제는 검색할 단어를 합치는 순간 그 단어의 출처를 잊는 데 있다. 모두 같은 무게로 세면 사용자가 입력한 코드 하나보다 별칭 여러 개가 더 높은 점수를 받을 수 있다. 그래서 파일 검색 서비스는 합쳐진 검색어 목록과 별도로, 사용자가 입력하지 않은 확장어 집합을 유지한다.
// 제품의 검색어 분리 로직을 옮긴 설명용 코드
const searchTerms = [...new Set([...userTerms, ...expandedTerms])];
const expansionTerms = new Set(
expandedTerms.filter(term => !userTerms.includes(term)),
);
searchTerms는 실제 문자열 검색에 넘길 목록이다. expansionTerms는 일치한 단어에 확장어 가중치를 적용할지 판단할 때 쓴다. 사전이 사용자 입력과 똑같은 문자열을 반환해도 검색 목록에는 한 번만 남고, 확장 전용 집합에서는 빠진다. 직접 입력한 단어가 사전에도 있다는 이유로 가중치가 낮아지지 않는 구조다.
여기서 중복 제거는 문자열 값의 비교다. Set이 의미가 비슷한 단어를 알아서 합쳐 주는 것은 아니며, 이 코드가 대소문자나 형태소까지 정규화하는 것도 아니다.
원래 검색어에 더 큰 가중치
스니펫은 검색 결과에서 본문 일부를 잘라 보여주는 단위다. SnippetAssembler는 스니펫에 일치한 단어를 중복 없이 모은 뒤 사용자 입력과 확장 전용 일치로 나눠 lexicalScore를 계산한다. 같은 단어의 반복 등장 횟수를 그대로 사용자 일치 개수로 세지는 않는다.
확장어 상한은 MAX_EXPANSION_TERMS = 32이고, 확장어 하나의 가중치는 1 / (MAX_EXPANSION_TERMS + 1)이다. 사용자 일치에는 단어당 1을 더한다. 실제 점수식에는 하이라이트 개수에 따른 보정도 포함된다.
사용자 일치 개수
+ 확장 전용 일치 개수 × 확장어 가중치
+ (하이라이트 개수 / 100) × 확장어 가중치
확장어 일치 항만 보면 상한까지 채워도 사용자 일치 하나보다 작다. 다만 실제 점수에는 하이라이트 보정이 더해지므로, 이 비교만으로 모든 입력에서 순위가 보장된다고 결론 내리면 안 된다. 반복 일치나 하이라이트가 많은 경우까지 보장하려면 별도의 경계 검증이 필요하다.
파일 검색 결과는 lexicalScore 내림차순으로 정렬한다. 점수가 같으면 priority 오름차순, 문서 날짜 내림차순, fileId 사전순으로 비교한다. 이 글의 점수는 파일 검색의 스니펫 점수이며 벡터 검색이나 모든 검색 모드의 통합 순위를 뜻하지 않는다.
실제 조립 코드로 확인한 순위
실행 예시에서는 검색 엔진이 반환할 일치 위치를 직접 만들고 실제 SnippetAssembler.assemble()에 넣었다. 문서마다 명시적인 본문 구간을 지정해 일치 항목들이 한 스니펫으로 묶이도록 했다. 사전 확장 결과도 고정했으므로, 이 실행이 검증하는 것은 동의어의 적절성이나 검색 엔진의 탐지 능력이 아니라 일치 이후의 점수 계산이다.
| 문서 | 설명용 본문 | 일치 유형 |
|---|---|---|
| A | SETUP01 설치 절차 | 사용자 입력 SETUP01 |
| B | 설치 안내 / 설치 설명서 / 셋업 가이드 | 확장 전용 표현 |
실행 결과에서 문서 A의 점수가 문서 B보다 높았다. 점수는 소수점 아래 여섯 자리로 반올림했다.
A 1.000303
B 0.091818
확장어를 상한인 32개까지 채운 별도 입력도 확인했다. 서로 겹치지 않는 확장어가 한 번씩 일치하는 스니펫의 점수는 0.979394였고, 사용자 입력 하나가 일치한 A보다 낮았다. 두 비교는 실제 조립 코드로 실행한 결과이지, 사용자 만족도나 검색 정확도를 측정한 수치가 아니다.
저장소의 회귀 테스트도 사용자 입력 일치 문서를 별칭만 일치하는 문서보다 먼저 반환하는지 확인하도록 작성돼 있다. 이번 검증에서는 그 테스트의 코드와 서비스 연결을 읽고, 조립 코드를 분리 실행했다. 회귀 테스트 전체나 실제 ripgrep 프로세스까지 실행한 것으로 해석해서는 안 된다.
사전이 입력 제한을 바꾸지 않도록
확장어를 추가하는 시점도 중요하다. 파일 검색 서비스는 사용자 입력을 먼저 검증한 뒤 사전 확장을 적용한다. 사용자가 보낼 수 있는 검색어는 최대 16개이며 이 상한에 사전 확장어는 포함하지 않는다.
사용자 입력 16개에 확장어 2개가 붙는다면 검색기에 넘길 목록은 중복이 없을 때 18개가 된다. 반대로 사용자가 처음부터 17개를 보내면 여전히 SEARCH_INVALID_QUERY로 거부한다. 사전을 켰다고 사용자가 입력할 수 있는 단어 수가 줄어서도, 입력 제한이 느슨해져서도 안 된다.
확장 실패도 원래 검색과 구분한다. 확장 서비스는 분석기나 사전 조회 실패를 빈 확장어 목록과 사유로 반환하는 계약을 갖고 있다. 파일 검색은 그 결과를 받으면 원래 검색어로 계속 진행한다. 파일 검색 호출부가 임의의 예외를 모두 삼키는 방식은 아니므로, 이 동작을 검색 중 어떤 오류가 나도 성공한다는 뜻으로 넓혀 읽으면 안 된다.
더 찾는 기능과 먼저 보여줄 기준
동의어 사전은 사용자가 입력하지 않은 표현을 검색에 보탠다. 그만큼 결과를 정렬할 때는 직접 입력한 단어와 사전이 추가한 단어를 구분할 필요가 있다.
DeepConnector의 파일 검색에서는 그 구분이 확장 전용 집합과 스니펫 가중치로 이어진다. 사전의 단어 수만 늘리는 대신, 원래 검색어의 우선순위와 사용자 입력 상한, 확장이 없어도 계속할 수 있는 검색 경로를 함께 검토해야 한다.
