다국어 인도 환경을 위한 전자정부: 어휘 기반 검색에서 희소-벡터 하이브리드 검색으로의 전환
요약
본 글은 다국어 인도 환경의 전자정부 검색 시스템 개선 방안을 제시합니다. 기존의 어휘 기반(BM25) 검색 한계를 극복하기 위해, 희소(sparse) 및 밀집(dense) 표현을 결합한 하이브리드 검색 기법으로 전환하는 과정을 설명합니다. 이를 통해 다양한 형태의 쿼리 불일치에 대응하여 사용자 경험을 향상시키는 것이 목표입니다.
핵심 포인트
- 다국어 환경에서 단순 키워드 매칭은 한계가 명확함.
- BM25 기반 레거시 시스템에서 하이브리드 검색으로 전환하는 과정이 핵심.
- 희소(sparse)와 밀집(dense) 표현을 결합하여 검색 성능을 강화할 수 있음.
- 실제 정부 제도 데이터셋을 활용하여 프로젝트를 진행했음.
다국어 인도 환경을 위한 전자정부: 어휘 기반 검색에서 희소-벡터 하이브리드 검색으로의 전환
시민이 정부 제도(scheme)를 검색할 때, 반드시 정부가 그 제도를 작성한 방식대로 검색하는 것은 아닙니다.

그들은 다음과 같이 검색할 수 있습니다:
farmer income support scheme
또는:
kisan ko har saal paisa milne wali scheme
또는:
किसानों को हर साल पैसे देने वाली योजना
혹은 다음과 같이 검색할 수도 있습니다:
farmer ko 6000 rupees wali yojna
근본적인 의도(intent)는 거의 동일할 수 있습니다.
단어들은 그렇지 않습니다.
그리고 이 코퍼스에 수천 개의 정부 제도가 포함되어 있을 때, 이는 흥미로운 검색 문제로 부상합니다.
이 프로젝트를 위해 저는 기존의 어휘 기반 검색 시스템이 얼마나 멀리까지 갈 수 있는지, 어디서부터 무너지기 시작하는지, 그리고 학습된 희소(sparse) 및 밀집(dense) 표현을 결합하는 것이 다국어 쿼리에 대해 검색을 더 강력하게 만들 수 있는지 이해하고 싶었습니다.
이 실험의 프로젝트 개요는 특히 BM25 기반 레거시 검색 설정에서 희소-벡터 및 하이브리드 검색으로 전환하는 데 초점을 맞추고 있으며, 다국어, 음역(transliterated) 및 어휘 불일치 쿼리를 테스트합니다.
그래서 즉시 임베딩을 시작하는 대신, 저는 가장 간단한 질문부터 시작했습니다:
키워드 검색이 실제로 얼마나 잘 작동할 수 있을까?
1. 데이터셋
저는 합성된 정부 제도를 만들고 싶지 않았습니다.
검색 코퍼스를 위해 Indian Government MyScheme Dataset을 사용했습니다.
해당 데이터셋에는 **2,066개의 정부 제도(government schemes)**가 포함되어 있습니다. 여기에는 중앙정부 제도 527개와 주/연방지 제도 1,539개가 있습니다. 이 데이터셋은 제도명, 부처/주, 카테고리, 설명, 자격 요건, 혜택, 신청 절차, 필요 서류, FAQ 및 공식 링크를 포함한 16개의 필드를 제공합니다. 이 데이터셋은 MyScheme 포털에서 파생되었으며 CSV와 JSON 형식으로 제공됩니다.
이는 장난감 코퍼스(toy corpus)보다 훨씬 유용한 자료를 제공한다는 것을 의미합니다.

가장 먼저 제가 한 일은 데이터셋을 불러오는 것이었습니다.
import pandas as pd
DATA_PATH = "gov_myscheme_data.csv"
...
어떤 검색 시스템도 구축하기 전에, 실제로 어떤 정보들이 사용 가능한지 이해하고 싶었습니다.
df.columns.tolist()
중요한 필드들은 대략 다음과 같았습니다:
Scheme Name
Scheme Slug
Level
...
이것은 중요합니다. 왜냐하면 정부 제도는 하나의 문장으로 표현되지 않기 때문입니다.
정보가 여러 필드에 걸쳐 분산되어 있습니다.
따라서 검색을 위해 단일 검색 가능한 텍스트 표현(searchable text representation)을 만들었습니다.
TEXT_COLUMNS = [
"Scheme Name",
"Tags / Categories",
...
이제 각 제도는 검색 가능한 문서(document)를 갖게 됩니다:
Scheme Name
+
Categories
...
이것이 모든 검색 접근 방식의 공통 입력값이 됩니다.
2. 방언 어휘 문제 (The Vernacular Vocabulary Problem)
가상의 제도 문서에 다음 내용이 포함되어 있다고 가정해 봅시다:
Pradhan Mantri Kisan Samman Nidhi
The scheme provides income support to eligible
...
사용자는 다음과 같이 검색할 수 있습니다:
PM Kisan farmer income support
여기에는 명확한 어휘적 중복(lexical overlap)이 존재합니다.
하지만 다음은 어떨까요:
yearly cash transfer for cultivators
이제 단어들이 다릅니다.
또는:
kisan ko har saal 6000 rupaye kaise milega
이제 우리는 음역된(transliterated) 힌디어와 마주하고 있습니다.
또는:
किसानों को हर साल 6000 रुपये की सहायता
이제 스크립트 자체가 바뀌었습니다.
이것은 우리에게 여러 가지 다른 검색 문제를 제시합니다.
그래서 저는 벡터 검색으로 바로 넘어가고 싶지 않았습니다.
먼저 기준선(baseline)을 설정하고 싶었습니다.
3. BM25 기준선 구축
BM25는 여전히 텍스트 검색에 매우 유용한 기준선입니다.
기본 아이디어는 간단합니다:
Query
↓
Tokenize
...
저는 기준선으로 rank_bm25를 사용했습니다.
from rank_bm25 import BM25Okapi
먼저, 인도 유니코드 문자(Indic Unicode characters)를 완전히 무시하지 않는 토크나이저가 필요했습니다.
import re
TOKEN_RE = re.compile(
...
그런 다음 코퍼스(corpus)를 구축했습니다.
tokenized_corpus = [
tokenize(text)
for text in df["search_text"]
...
그리고 검색은 간단합니다:
def bm25_search(query, top_k=5):
query_tokens = tokenize(query)
...
이제 테스트할 수 있습니다:
query = "farmer income support scheme"
results = bm25_search(
...
문서에 사용된 전문 용어(terminology)를 포함하는 쿼리에는 BM25가 놀라울 정도로 잘 작동할 수 있습니다.
그것이 문제는 아닙니다.
문제는 시민이 그 전문 용어를 모를 때 발생합니다.
4. 왜 어휘 기반 검색이 무너지기 시작하는가
다음 것을 고려해 봅시다:
Document: financial assistance to eligible farmer families
그리고:
Query: yearly cash transfer for cultivators
BM25는 다음을 찾고 있습니다:
yearly
cash
transfer
...
하지만 문서에는 대신 다음과 같은 내용이 포함되어 있을 수 있습니다:
financial
assistance
farmer
...
개념(concepts)은 관련성이 있습니다.
어휘(vocabulary)는 그렇지 않습니다.
전통적인 역색인(inverted index)은 근본적으로 용어 일치(matching terms)를 중심으로 구축됩니다.
따라서 검색 경로는 다음과 같습니다:
Query Term
↓
Inverted Index
...
이것은 추가적인 메커니즘이 그 연결고리를 제공하지 않는 한, 다음을 본질적으로 이해하지 못합니다:
cash transfer
≈
financial assistance
그리고 여기서 학습된 희소 검색(learned sparse retrieval)이 흥미로워집니다.
5. 정확한 용어(Exact Terms)를 넘어서기
텍스트를 단순히 단어 목록으로 표현하는 대신, 학습된 희소 모델을 사용할 수 있습니다.
희소 검색 실험에 사용되는 모델은 다음과 같습니다:
SPARSE_MODEL = (
"prithivida/Splade_PP_en_v1"
)
FastEmbed를 사용하여:
from fastembed import SparseTextEmbedding
sparse_model = SparseTextEmbedding(...)
이제 문서를 인코딩할 수 있습니다:
sparse_embeddings = list(
sparse_model.embed(
df["search_text"].tolist(),
...)
밀집 임베딩(dense embedding)은 개념적으로 다음과 같을 것입니다:
[0.12, -0.08, 0.31, ...]
반면 희소 표현(sparse representation)은 상대적으로 적은 수의 활성 차원(active dimensions)을 포함합니다:
indices = [12, 183, 902, ...]
values = [0.21, 0.74, 0.32, ...]
중요한 아이디어는 모델이 모든 토큰을 동등하게 취급하기보다는 어떤 용어/특징(terms/features)이 중요한지 학습한다는 것입니다.
따라서 검색 문제는 다음과 같이 됩니다:
정부 문서 (Government Document)
↓
학습된 희소 인코더 (Learned Sparse Encoder)
...
그리고 쿼리(query)에 대해서는:
시민 쿼리 (Citizen Query)
↓
학습된 희소 인코더 (Learned Sparse Encoder)
...
따라서 이 표현은 모든 문서에 대해 거대한 밀집 벡터를 필요로 하지 않으면서 유용한 어휘적(lexical) 및 학습된 확장 신호(learned expansion signals)를 포착할 수 있습니다.
6. 한 가지 중요한 제한점
여기에는 미묘하지만 중요한 세부 사항이 있습니다.
제가 사용한 희소 모델은 다음과 같습니다:
prithivida/Splade_PP_en_v1
따라서 이것을 보편적인 다국어 솔루션(universal multilingual solution)이라고 설명하지는 않을 것입니다.
이것이 중요한 이유는 우리의 문제가 **다양한 방언의 인도 (vernacular India)**이기 때문입니다.
주로 영어 검색을 중심으로 훈련된 희소 모델과 다국어 밀집 모델은 문제의 서로 다른 부분을 해결하고 있습니다.
이는 우리에게 또 다른 질문을 던져줍니다:
사용자가 언어를 변경하면 어떻게 될까요?
7. 다국어 밀집 임베딩 추가하기
밀집 표현(dense representation)의 경우, 저는 다음을 사용했습니다:
from sentence_transformers import SentenceTransformer
dense_model = SentenceTransformer(...)
이제 코퍼스를 임베딩할 수 있습니다:
그리고 하나의 질의(query)에 대해서는 다음과 같이 임베딩을 생성합니다:
query_embedding = dense_model.encode(
[query],
normalize_embeddings=True
...
이제 표현은 더 이상 단순히 단어 일치 여부에 기반하지 않습니다.
모델이 텍스트의 의미적(semantic) 의미를 나타내려고 시도하는 것입니다.
따라서 다음과 같은 질의:
yearly financial support for farmers
와:
kisan ko har saal paisa milne wali scheme
은 임베딩 공간에서 잠재적으로 가까운 영역을 차지할 수 있습니다.
그리고 이것이 중요한 차이점입니다.
BM25
↓
단어들이 일치하는가?
...
이 질문들 중 어느 것도 동일하지 않습니다.
8. 여러 표현(Representation)을 결합하기
이 시점에서 저는 다음과 같은 것을 가지고 있었습니다:

저는 이 표현들이 함께 존재하기를 원했습니다.
바로 이때 데이터베이스 아키텍처가 유용해집니다.
어휘적(lexical) 검색을 위한 하나의 시스템과 의미적(semantic) 검색을 위한 또 다른 시스템을 유지하는 대신, 동일한 지점(point)에 대해 여러 개의 이름이 지정된 벡터(named vectors)를 저장할 수 있습니다.
개념적으로:
Qdrant Point
│
├── Dense Vector
...
Qdrant의 하이브리드 검색 API는 여러 개의 이름이 지정된 벡터와 prefetch 쿼리를 지원하여, 서로 다른 표현들을 검색하고 그 결과를 융합(fused)할 수 있게 합니다.
9. 컬렉션 생성하기
컬렉션은 밀집 벡터(dense vectors)와 희소 벡터(sparse vectors) 모두로 구성될 수 있습니다.
from qdrant_client import QdrantClient, models
client = QdrantClient(
...
그런 다음:
client.create_collection(
collection_name=
그것은 표현(representation)입니다.
이제 하나의 스킴이 다음을 가질 수 있습니다:
[](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fis43psx0jbddz85qv0m4.png)
## 10. 페이로드(Payload) 준비하기
벡터는 검색에 유용합니다.
하지만 실제 스킴 정보는 여전히 반환되어야 합니다.
그래서 관련 메타데이터를 페이로드로 저장했습니다.
points = []
for i, row in df.iterrows():
...
마지막으로:
client.upsert(
collection_name="gov_schemes",
points=points
...
이제 검색과 메타데이터가 함께 존재합니다.
## 11. 밀집 표현(Dense Representation)으로 검색하기
밀집 검색부터 시작하겠습니다.
query = (
"government scheme giving financial "
"support to farmers every year"
...
그런 다음:
for result in dense_results:
print(
...
여기서 의미론적 검색(semantic retrieval)이 유용해지기 시작합니다.
사용자는 문서 안에 나타나는 정확한 구문을 알 필요가 없습니다.
## 12. 희소 표현(Sparse Representation)으로 검색하기
동일한 쿼리가 희소 인코더를 통과합니다.
query_sparse = list(
sparse_model.embed(
[query]
...
그런 다음:
sparse_results = client.query_points(
collection_name="gov_schemes",
...
이제 두 개의 순위가 매겨진 목록을 갖게 되었습니다.
Dense
- Scheme A
...
남은 질문은 다음과 같습니다:
> **두 시스템 모두 유용한 정보를 가지고 있다면, 왜 하나만 선택해야 할까요?**
## 13. 제가 단순히 점수(Scores)를 추가하지 않은 이유
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
