microsearch는 검색 엔진 내부를 직접 이해하기 위한 장난감 구현으로, 핵심 검색 엔진 클래스는 80줄 미만이지만 크롤러·API·HTML 템플릿까지 포함하면 프로젝트는 더 큼- 작은 웹사이트와 블로그가 대형 검색엔진에서 잘 발견되지 않는 문제를 배경으로, 642개 RSS 피드에서 글을 수집해 검색 데이터를 만듦
asyncio기반 비동기 크롤링으로 수집 시간이 20분에서 20초로 줄었고, 정리된 본문은 Parquet 데이터로 저장됨- 검색은 단어를 URL별 등장 횟수에 연결하는 역색인 위에서 동작하며, 결과 정렬에는 링크 기반 PageRank 대신 콘텐츠 기반 BM25를 사용함
- FastAPI UI로 검색창과 결과 페이지를 제공하지만, 쿼리 연산자·n-gram 색인·쿼리/문서 확장·크롤링 중 색인 같은 기능은 아직 없음
microsearch의 목표와 범위
microsearch는 GitHub 저장소에 공개된 Python 검색 엔진 구현임- 목적은 프로덕션용 검색엔진이 아니라, 검색엔진이 내부에서 어떻게 동작하는지 보여주는 사용 가능한 장난감 예제를 만드는 것임
- 검색 대상은 Google SEO 경쟁에서 잘 발견되지 않는 작은 웹사이트와 블로그에 가까움
- 핵심 검색 엔진 구현은 80줄 미만이지만, 데이터 크롤러·API·HTML 템플릿 같은 보조 코드를 포함하면 프로젝트 전체는 더 큼
- 구현은 Solr와 Lucene을 다루며 검색 엔진 동작을 더 깊게 이해하려는 과정에서 만들어짐
RSS 기반 크롤러
- 검색할 데이터를 만들기 위해 블로그 RSS 피드를 크롤링함
- 사용한 피드는 총 642개 RSS 피드임
- 약 100개는 ML, 데이터 과학, 수학 등 직접 읽는 블로그
- 나머지 약 500개는 surprisetalk blogs.hn 프로젝트에서 가져옴
- 크롤링은 각 RSS 피드에서 글 URL을 추출하고, 글 HTML을 내려받은 뒤 본문 텍스트를 정리하는 흐름임
- HTML 정리는
BeautifulSoup으로script와style을 제거하고, 줄바꿈과 공백을 정리해 텍스트로 변환함 aiohttp와asyncio를 사용한 비동기 크롤링으로 실행 시간이 20분에서 20초로 줄어듦- 결과는 URL과 정리된 본문을 담은
DataFrame으로 만든 뒤output.parquet에 저장함
역색인 구조
- 검색 엔진의 첫 핵심 데이터 구조는 역색인임
- 역색인은 키워드를 문서에 매핑해, 특정 단어가 어떤 문서에 등장하는지 빠르게 찾게 해줌
- 구현은
dict[str, dict[str, int]]형태의defaultdict를 사용함- 바깥 키는 단어
- 안쪽 키는 URL
- 안쪽 값은 해당 단어가 그 URL의 문서에 등장한 횟수
SearchEngine클래스는 두 개의 내부 딕셔너리를 가짐_index: 단어별 URL 등장 횟수 저장_documents: URL별 원문 콘텐츠 저장
index(url, content)는 콘텐츠를 정규화한 뒤 공백으로 분리하고, 각 단어의 URL별 등장 횟수를 증가시킴bulk_index()는 URL과 콘텐츠 목록을 받아 여러 문서를 한 번에 색인함get_urls(keyword)는 키워드를 정규화한 뒤 해당 단어를 포함하는 URL과 등장 횟수를 반환함
문자열 정규화와 기본 검색
- 문자열 정규화는 문장 부호를 공백으로 바꾸고, 중복 공백을 정리한 뒤 소문자로 변환함
- 대소문자 차이를 줄이기 위해
Foo와foo는 같은 키워드로 처리됨 - 예시 문서 두 개를 색인하면
foo검색 결과는 두 문서 모두를 반환함Foo:Hello, World! My name is Foo!Bar:Hello, World! My name is Bar, I'm not Foo!
- 이 단계에서는 문서가 검색어를 포함하는지와 몇 번 포함하는지만 알 수 있으므로, 결과 순서를 정하려면 별도 랭킹이 필요함
BM25 랭커
- 검색 결과 정렬에는 BM25를 사용함
- PageRank는 링크를 기반으로 문서를 랭킹하지만, BM25는 문서 콘텐츠를 기반으로 점수를 계산함
SearchEngine은 BM25 계산을 위해 기본 파라미터k1=1.5,b=0.75를 가짐- 클래스는 랭킹 계산에 필요한 속성을 제공함
posts: 색인된 URL 목록number_of_documents: 전체 문서 수avdl: 평균 문서 길이
idf(kw)는 특정 키워드의 역문서빈도를 계산함- 전체 문서 수
N - 해당 키워드를 포함하는 문서 수
n_kw log((N - n_kw + 0.5) / (n_kw + 0.5) + 1)수식을 사용함
- 전체 문서 수
bm25(kw)는 해당 키워드를 포함하는 URL마다 BM25 점수를 계산함search(query)는 쿼리를 정규화하고 단어로 나눈 뒤, 각 단어의 BM25 점수를 URL별로 합산해 반환함- 예시에서
foo만 검색하면Foo문서 점수가Bar보다 높고,foo bar를 검색하면Bar문서 점수가 더 높아짐
FastAPI 인터페이스
- 검색 엔진은 작은 FastAPI 앱으로 노출됨
- 앱은
SearchEngine인스턴스를 만들고, 시작 시 Parquet 데이터에서 URL과 콘텐츠를 읽어bulk_index()로 색인함 - 주요 라우트는 세 가지임
/: 검색 페이지를 렌더링하고 색인된 글 목록을 전달함/results/{query}: 쿼리를 검색하고 상위 5개 URL을 결과 페이지에 표시함/about: 소개 페이지를 렌더링함
- 결과는 점수 기준 내림차순으로 정렬한 뒤 top-N URL만 선택함
- UI와 UX는 개선 여지가 크지만, 검색은 빠르게 동작하고 결과도 나쁘지 않음
빠진 기능과 한계
- 구현에는 실제 검색 엔진에서 기대할 수 있는 여러 기능이 빠져 있음
- 쿼리 연산자가 없음
- 예를 들어 Google의
how to build a search engine -solr처럼 특정 단어를 제외하는 검색을 지원하지 않음
- 예를 들어 Google의
- n-gram 색인이 없음
"search engine"처럼 두 단어가 특정 순서로 등장하는 문서만 찾는 방식이 지원되지 않음
- 쿼리 또는 문서 확장이 없음
engine을 검색해도engines가 들어간 문서는 자동으로 검색되지 않음
- 크롤링과 색인이 분리돼 있음
- 문서를 받는 즉시 색인하는 방식으로 통합할 수 있고, 이 과정도 비동기로 만들 수 있음
다음 단계
- 프로젝트를 통해 Solr가 내부에서 어떻게 동작하는지에 대한 직관이 더 생김
- IO 중심 작업에서는 비동기 코드가 큰 효과를 낸다는 점도 확인됨
- 다음 단계는 검색 엔진에 시맨틱 검색 기능을 추가하는 것임
- 임베딩 모델과 ANN을 실험해 왔고, 그 기능을
microsearch에 넣는 것이 다음 작업임