순수 파이썬으로 검색 엔진 구축하기: 단계별 안내 - 종속성 없음
Olivia Novak
Dev Intern · Leapcell

순수 파이썬으로 검색 엔진 구축하기: 단계별 안내 - 종속성 없음
오늘날 정보 폭발의 시대에 검색 엔진은 사람들이 정보에 접근하는 주요 방법이 되었습니다. Google 및 Bing과 같은 상용 검색 엔진은 복잡한 기술 아키텍처를 가지고 있지만, 핵심 원리는 기본 정보 검색 기술을 통해 이해할 수 있습니다. 이 기사에서는 타사 종속성 없이 Python의 표준 라이브러리만 사용하여 스크래치에서 TF-IDF 알고리즘 기반 영어 검색 엔진을 구축하고 주요 역 인덱스 구조를 CSV 형식으로 저장하는 과정을 안내합니다. 이 연습을 통해 검색 엔진 작동 방식에 대한 깊은 이해를 얻고 텍스트 처리, 인덱스 구성 및 관련성 계산의 핵심 기술을 마스터할 수 있습니다.
검색 엔진의 핵심 구성 요소
완전한 검색 엔진은 일반적으로 문서 처리 모듈, 인덱스 구성 모듈, 쿼리 처리 모듈 및 순위 모듈의 네 가지 핵심 모듈로 구성됩니다. 타사 라이브러리에 의존하는 구현과 달리 순수한 Python 구현에서는 각 단계의 세부 사항을 수동으로 처리해야 하므로 각 단계의 기본 원리를 더 깊이 이해할 수 있습니다.
문서 처리 모듈은 원시 텍스트를 토큰화, 노이즈 제거(예: 구두점) 및 정규화(예: 대소문자 변환)와 같은 연산을 포함하여 계산에 적합한 구조화된 데이터로 변환합니다. 인덱스 구성 모듈은 검색 엔진의 핵심이며, 각 용어가 어떤 문서에 나타나는지 및 해당 위치를 기록하는 역 인덱스 구축을 통해 빠른 쿼리를 가능하게 합니다. 쿼리 처리 모듈은 사용자 입력 쿼리를 수신하고 인덱스에서 용어를 일치시키기 위해 문서 처리와 동일한 정규화 작업을 수행합니다. 순위 모듈은 TF-IDF 알고리즘을 사용하여 쿼리 용어와 각 문서 간의 관련성 점수를 계산하고 점수별로 정렬된 결과를 반환합니다.
TF-IDF 알고리즘의 원리
TF-IDF (Term Frequency-Inverse Document Frequency)는 문서 모음에서 용어의 중요도를 평가하는 데 사용되는 통계적 방법입니다. 핵심 아이디어는 용어의 중요도는 특정 문서에서의 빈도에 비례하고 전체 문서 모음에서의 빈도에 반비례한다는 것입니다.
용어 빈도 (TF) 계산
용어 빈도는 특정 문서에서 용어가 나타나는 횟수를 나타냅니다. 결과에 대한 문서 길이의 영향을 피하기 위해 일반적으로 정규화가 적용됩니다.
TF(t,d) = 문서 d에서 용어 t가 나타나는 횟수 / 문서 d의 총 용어 수
예를 들어 100개의 용어를 포함하는 문서에서 "학습"이 5번 나타나면 해당 용어 빈도는 5/100 = 0.05입니다.
역 문서 빈도 (IDF) 계산
역 문서 빈도는 용어의 판별력을 측정하며 다음과 같이 계산됩니다.
IDF(t) = log(총 문서 수 / 용어 t를 포함하는 문서 수)
용어가 대부분의 문서에 나타나면 IDF 값이 낮아 판별력이 약함을 나타냅니다. 반대로 용어가 소수의 문서에만 나타나면 IDF 값이 높아 판별력이 강함을 나타냅니다. 예를 들어 총 10개의 문서가 있고 "기계"가 그중 3개에 나타나면 IDF 값은 log(10/3) ≈ 1.20397입니다.
TF-IDF 값 계산
TF-IDF 값은 용어 빈도와 역 문서 빈도의 곱입니다.
TF-IDF(t,d) = TF(t,d) × IDF(t)
이 값은 문서 d에서 용어 t의 중요도를 포괄적으로 반영하며 문서와 쿼리 간의 관련성을 측정하는 중요한 지표입니다.
역 인덱스 설계
역 인덱스는 검색 엔진에서 빠른 쿼리를 위한 핵심 데이터 구조이며, 각 용어와 해당 용어가 나타나는 문서 및 위치를 기록합니다. 순수한 Python 구현에서는 합리적인 역 인덱스 구조를 설계하고 지속성을 위해 CSV 형식으로 저장해야 합니다.
역 인덱스의 기본 구조에는 용어, 문서 ID (doc_id) 및 위치의 세 부분이 포함됩니다. 위치 정보는 용어가 문서에 나타나는 특정 위치를 기록하여 후속 구문 쿼리 및 근접 쿼리를 용이하게 합니다.
CSV 파일 형식은 다음과 같이 설계되었습니다.
term,doc_id,positions machine,0,"[1,5]" learning,0,"[2,8]" ...
이 형식을 사용하면 Python의 표준 csv 모듈을 사용하여 읽기 및 쓰기 작업을 수행할 수 있습니다. 다른 문서에서 용어가 발생하는 각 항목은 행으로 기록되며, positions 필드는 위치 목록을 문자열로 저장합니다.
순수 파이썬 구현 단계
다음으로 문서 전처리, 역 인덱스 구성, TF-IDF 계산, 쿼리 처리 및 결과 순위 지정을 포함하여 순수한 Python을 사용하여 스크래치에서 TF-IDF 기반 영어 검색 엔진을 구현하는 방법을 자세히 설명합니다.
1단계: 문서 전처리
문서 전처리는 원시 텍스트를 구조화된 데이터로 변환하며, 주로 다음 작업을 포함합니다.
- 대소문자 변환: "Machine"과 "machine"을 다른 용어로 처리하지 않도록 모든 텍스트를 소문자로 변환합니다.
- 구두점 제거: 노이즈를 줄이기 위해 텍스트에서 구두점을 제거합니다.
- 토큰화: 연속적인 텍스트를 개별 용어로 분리합니다.
- 불용어 제거: "the" 및 "and"와 같이 실질적인 의미가 없는 고빈도 단어를 필터링합니다.
- 간단한 스테밍: 용어를 어근 형태로 줄입니다 (단순화된 구현).
다음은 구현 코드입니다.
import string # 영어 불용어 집합 정의 STOP_WORDS = { 'a', 'an', 'and', 'the', 'or', 'of', 'to', 'in', 'for', 'on', 'with', 'at', 'by', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'its', 'our', 'their', 'this', 'that', 'these', 'those', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'may', 'might', 'must', 'can', 'could', 'as', 'but', 'if', 'or', 'because', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very' } def preprocess_text(text): """텍스트 전처리: 대소문자 변환, 구두점 제거, 토큰화, 불용어 제거""" # 소문자로 변환 text = text.lower() # 구두점 제거 translator = str.maketrans('', '', string.punctuation) text = text.translate(translator) # 토큰화 (간단한 공백 분할; 실제 응용 프로그램에서는 더 복잡한 로직을 사용할 수 있음) tokens = text.split() # 불용어 및 빈 문자열 제거 tokens = [token for token in tokens if token not in STOP_WORDS and token.strip() != ''] # 간단한 스테밍 (단순화된 버전) tokens = [stem_token(token) for token in tokens] return tokens def stem_token(token): """간단한 스테밍 함수 (실제 응용 프로그램에서는 더 복잡한 알고리즘을 사용할 수 있음)""" # 일반적인 접미사 처리 suffixes = ['ing', 'ly', 'ed', 'es', 's'] for suffix in suffixes: if token.endswith(suffix) and len(token) > len(suffix): return token[:-len(suffix)] return token # 전처리 함수 테스트 sample_text = "Machine learning is a subset of artificial intelligence focused on developing algorithms that learn from data." processed_tokens = preprocess_text(sample_text) print("전처리된 용어:", processed_tokens)
이 전처리 함수는 기본 텍스트 정리 및 정규화를 구현하여 후속 인덱스 구성 및 TF-IDF 계산을 위한 기반을 마련합니다. 여기서 토큰화 및 스테밍은 단순화된 구현입니다. 실제 응용 프로그램에서는 정확도를 높이기 위해 더 복잡한 알고리즘을 사용할 수 있습니다.
2단계: 역 인덱스 구축 및 CSV로 저장
역 인덱스를 구축하는 프로세스에는 모든 문서를 반복하고 각 용어가 나타나는 문서 ID와 위치 정보를 기록하고 결과를 CSV 파일로 저장하는 것이 포함됩니다.
import csv from collections import defaultdict def build_inverted_index(documents): """역 인덱스 구축 및 CSV 파일로 저장""" inverted_index = defaultdict(list) # 구조: {term: [(doc_id, positions), ...]} for doc_id, doc in enumerate(documents): # 문서 전처리 tokens = preprocess_text(doc) # 현재 문서에서 각 용어의 위치 기록 term_positions = defaultdict(list) for pos, term in enumerate(tokens): term_positions[term].append(pos) # 역 인덱스 업데이트 for term, positions in term_positions.items(): inverted_index[term].append((doc_id, positions)) # 역 인덱스를 CSV 파일로 저장 with open('inverted_index.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['term', 'doc_id', 'positions']) for term, doc_info in inverted_index.items(): for doc_id, positions in doc_info: # 저장을 위해 위치 목록을 문자열로 변환 positions_str = str(positions) writer.writerow([term, doc_id, positions_str]) return inverted_index # 샘플 문서 모음 documents = [ "Machine learning is a subset of artificial intelligence focused on developing algorithms that learn from data.", "Artificial intelligence involves creating systems that can perform tasks requiring human intelligence.", "Deep learning is a type of machine learning based on artificial neural networks with multiple layers.", "Natural language processing allows computers to understand and generate human language.", "Computer vision enables machines to interpret and understand the visual world.", "Reinforcement learning is an area of machine learning concerned with how agents take actions in an environment.", "Supervised learning algorithms learn from labeled training data to make predictions on new data.", "Unsupervised learning deals with unlabeled data, finding patterns and structures within it.", "A neural network is a computational model inspired by the human brain's structure and function.", "Big data refers to large and complex data sets that require advanced processing techniques." ] # 역 인덱스 구축 inverted_index = build_inverted_index(documents) print(f"역 인덱스가 구축되었으며 {len(inverted_index)}개의 용어가 포함되어 있습니다")
이 코드는 먼저 각 문서를 반복하고, 전처리하고, 문서에서 각 용어의 위치를 기록하고, 이 정보를 역 인덱스로 구성하고 CSV 파일로 저장합니다. 역 인덱스는 키가 용어이고 값이 문서 ID와 위치 목록을 포함하는 튜플 목록인 사전으로 구성됩니다.
3단계: TF-IDF 값 계산
TF-IDF 값을 계산하려면 먼저 용어 빈도와 역 문서 빈도를 계산한 다음 그 곱을 계산해야 합니다. 순수한 Python 구현에서는 이러한 계산을 수동으로 수행해야 합니다.
def calculate_tfidf(documents, inverted_index): """각 문서에서 각 용어에 대한 TF-IDF 값 계산""" num_docs = len(documents) tfidf = {} # 구조: {doc_id: {term: tfidf_value, ...}, ...} # 각 문서의 총 용어 수 계산 doc_lengths = [] for doc in documents: tokens = preprocess_text(doc) doc_lengths.append(len(tokens)) # 각 용어에 대한 문서 빈도 계산 (용어를 포함하는 문서 수) doc_freq = {term: len(entries) for term, entries in inverted_index.items()} # TF-IDF 계산 for term, entries in inverted_index.items(): # IDF 계산 idf = math.log(num_docs / (doc_freq[term] + 1)) # 0으로 나누는 것을 방지하기 위해 +1 for doc_id, positions in entries: # TF 계산 tf = len(positions) / doc_lengths[doc_id] if doc_lengths[doc_id] > 0 else 0 # TF-IDF 계산 tfidf_value = tf * idf # 결과 저장 if doc_id not in tfidf: tfidf[doc_id] = {} tfidf[doc_id][term] = tfidf_value return tfidf import math # 로그 계산을 위해 math 라이브러리 가져오기 # TF-IDF 값 계산 tfidf_scores = calculate_tfidf(documents, inverted_index) print("TF-IDF 계산 완료")
이 코드는 먼저 각 문서의 길이 (전처리 후 총 용어 수)와 각 용어의 문서 빈도를 계산한 다음 용어 빈도와 역 문서 빈도를 각각 계산하고 마지막으로 그 곱을 계산하여 TF-IDF 값을 얻습니다. 계산 결과는 후속 쿼리에서 쉽게 사용할 수 있도록 중첩된 사전에 저장됩니다.
4단계: 쿼리 처리 및 결과 반환
쿼리 처리 모듈은 사용자 입력 쿼리를 전처리하고, 역 인덱스를 기반으로 쿼리 용어를 포함하는 문서를 찾고, 쿼리와 각 문서 간의 관련성 점수를 계산하고, 점수별로 정렬된 결과를 반환해야 합니다.
def search(query, documents, inverted_index, tfidf_scores, top_n=3): """쿼리 처리 및 가장 관련성이 높은 문서 반환""" # 쿼리 전처리 query_terms = preprocess_text(query) if not query_terms: return [] # 하나 이상의 쿼리 용어를 포함하는 문서 가져오기 relevant_docs = set() for term in query_terms: if term in inverted_index: for doc_id, _ in inverted_index[term]: relevant_docs.add(doc_id) relevant_docs = list(relevant_docs) # 쿼리와 각 관련 문서 간의 관련성 점수 계산 scores = [] for doc_id in relevant_docs: score = 0.0 for term in query_terms: if term in tfidf_scores.get(doc_id, {}): score += tfidf_scores[doc_id][term] # 점수 정규화 (쿼리 용어 수로 나누기) score /= len(query_terms) scores.append((doc_id, score)) # 점수별로 정렬 scores.sort(key=lambda x: x[1], reverse=True) # 상위 N개 결과 반환 results = [] for doc_id, score in scores[:top_n]: if score > 0: results.append({ 'document': documents[doc_id], 'score': score, 'doc_id': doc_id }) return results # 검색 기능 테스트 import math # math 라이브러리가 가져와 있는지 확인 query = "machine learning" results = search(query, documents, inverted_index, tfidf_scores) print(f"쿼리: {query}") for i, result in enumerate(results, 1): print(f"\n 결과 {i} (점수: {result['score']:.4f}):") print(result['document'])
이 코드는 먼저 쿼리 용어를 전처리한 다음 쿼리 용어를 포함하는 모든 문서를 찾고 각 문서와 쿼리 간의 관련성 점수를 계산 (여기서는 간단한 합산 방법을 사용)하고 마지막으로 점수별로 정렬된 결과를 반환합니다.
5단계: CSV에서 역 인덱스 로드
지속성을 위해 CSV 파일에서 역 인덱스를 로드할 수 있어야 합니다.
def load_inverted_index_from_csv(filename): """CSV 파일에서 역 인덱스 로드""" inverted_index = defaultdict(list) with open(filename, 'r', encoding='utf-8') as f: reader = csv.reader(f) next(reader) # 헤더 건너뛰기 for row in reader: term = row[0] doc_id = int(row[1]) # 위치 문자열을 목록으로 다시 변환 positions = eval(row[2]) # 참고: eval에는 보안 위험이 있습니다. 실제 응용 프로그램에서는 더 안전한 방법을 사용하십시오. inverted_index[term].append((doc_id, positions)) return inverted_index # 역 인덱스 로드 테스트 loaded_index = load_inverted_index_from_csv('inverted_index.csv') print(f"CSV에서 로드된 역 인덱스에는 {len(loaded_index)}개의 용어가 포함되어 있습니다")
이 코드는 CSV 파일에서 역 인덱스 데이터를 읽고, 위치 정보를 문자열에서 목록으로 다시 변환하고, 역 인덱스 구조를 재구성합니다. eval 함수를 사용하면 보안 위험이 있습니다. 실제 응용 프로그램에서는 더 안전한 방법 (예: 정규식 구문 분석)을 사용하여 위치 문자열을 변환할 수 있습니다.
성능 최적화 및 확장
순수한 Python으로 구현된 검색 엔진은 대규모 문서를 처리할 때 성능 문제가 발생할 수 있습니다. 다음은 몇 가지 최적화 제안 사항입니다.
- 인덱스 압축: 역 인덱스의 위치 정보는 델타 인코딩과 같은 방법을 사용하여 압축하여 저장 공간과 메모리 사용량을 줄일 수 있습니다.
- 캐싱 메커니즘: 자주 액세스하는 인덱스 부분을 메모리에 캐시하여 디스크 I/O 작업을 줄입니다.
- 병렬 컴퓨팅: TF-IDF 계산과 같이 시간이 오래 걸리는 작업을 수행할 때 Python의 multiprocessing 모듈을 사용하여 병렬 컴퓨팅을 수행합니다.
- 쿼리 최적화: 다중 용어 쿼리의 경우 관련성을 계산하기 전에 먼저 모든 쿼리 용어를 포함하는 문서 (교차)를 찾아 계산을 줄입니다.
또한 검색 엔진의 기능을 확장하여 구문 쿼리, 근접 쿼리, 부울 쿼리 등을 지원하여 검색 정확도와 유연성을 향상시킬 수 있습니다.
실제 응용 프로그램의 과제
실제 응용 프로그램에서 순수한 Python으로 구현된 검색 엔진은 다음과 같은 문제에 직면할 수 있습니다.
- 성능 제한: C++와 같은 컴파일된 언어에 비해 Python은 실행 효율성이 낮고 대규모 데이터를 처리할 때 병목 현상이 발생할 수 있습니다.
- 기능 제한: 순수한 Python 구현은 단어 의미 중의성 해소 및 개체 인식과 같은 복잡한 자연어 처리 작업에 어려움을 겪습니다.
- 확장성: 문서 및 어휘 수가 증가함에 따라 인덱스 구조가 빠르게 확장되어 보다 복잡한 분산 스토리지 및 컴퓨팅 아키텍처가 필요합니다.
따라서 실제 검색 엔진 개발에서는 Python의 사용 편의성과 다른 고성능 언어의 장점을 결합하여 하이브리드 아키텍처로 시스템을 구축합니다.
결론
이 기사를 통해 타사 라이브러리에 의존하지 않고 스크래치에서 TF-IDF 기반 영어 검색 엔진을 구축하고 주요 역 인덱스를 CSV 형식으로 저장했습니다. 이 과정을 통해 문서 전처리, 역 인덱스 구성, TF-IDF 계산 및 쿼리 처리와 같은 주요 단계를 포함하여 검색 엔진의 핵심 원리와 구현 세부 사항에 대한 깊은 이해를 얻을 수 있었습니다.
이 구현은 비교적 간단하지만 최신 검색 엔진의 기본 프레임워크를 다룹니다. 이 토대를 기반으로 기능을 더욱 확장하고 성능을 최적화하여 보다 강력한 검색 시스템을 구축할 수 있습니다. 학술 연구 또는 실제 응용 프로그램에 관계없이 이러한 기본 원리를 이해하는 것은 정보 검색 기술에 대한 지식을 심화시키는 중요한 단계입니다.
이 기사가 정보 검색 분야에 대한 문을 열어 검색 엔진 기술에 대한 관심과 탐구 욕구를 자극하기를 바랍니다. 정보 폭발의 시대에 정보 검색 기술을 마스터하면 정보를 보다 효율적으로 얻을 수 있을 뿐만 아니라 데이터 마이닝 및 인공 지능과 같은 분야의 연구를 위한 견고한 기반을 제공합니다.
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로 Python 서비스를 배포하기 위한 훌륭한 플랫폼인 **Leapcell**을 추천합니다.
🚀 좋아하는 언어로 구축하세요
JavaScript, Python, Go 또는 Rust로 간편하게 개발하세요.
🌍 무제한 프로젝트를 무료로 배포하세요
사용한 만큼만 지불하세요. 요청이나 요금이 없습니다.
⚡ 사용한 만큼 지불하고 숨겨진 비용이 없습니다.
유휴 요금이 없고 원활한 확장성만 있습니다.
🔹 트위터에서 팔로우하세요: @LeapcellHQ