검색 엔진(Search Engine)은 인터넷 상의 방대한 정보를 효율적으로 수집, 저장, 검색하여 사용자에게 필요한 정보를 빠르게 제공하는 시스템입니다.
크롤러(Crawler) 또는 스파이더(Spider)는 웹 페이지를 자동으로 탐색하고 데이터를 수집하는 역할을 합니다. 크롤러는 시작 URL 목록에서부터 시작하여 링크를 따라가며 웹 페이지를 방문하고 콘텐츠를 수집합니다.
@Service
public class CrawlerService {
private Set<String> visited = ConcurrentHashMap.newKeySet();
private Queue<String> queue = new ConcurrentLinkedQueue<>();
public void startCrawling(String startUrl) {
queue.add(startUrl);
while(!queue.isEmpty()) {
String url = queue.poll();
if (visited.contains(url)) continue;
visited.add(url);
// HTTP 요청 및 페이지 파싱 로직
// 새로운 링크를 queue에 추가
}
}
}
인덱싱은 수집된 웹 페이지의 콘텐츠를 분석하고 구조화하여 빠른 검색이 가능하도록 데이터베이스에 저장하는 과정입니다. 일반적으로 역인덱스(Inverted Index)를 사용하여 단어와 해당 단어가 포함된 문서 간의 매핑을 생성합니다.
구현 시 고려 사항
@Service
public class IndexingService {
private Map<String, Set<Long>> invertedIndex = new ConcurrentHashMap<>();
public void indexDocument(Long docId, String content) {
String[] tokens = content.toLowerCase().split("\\W+");
for(String token : tokens) {
invertedIndex.computeIfAbsent(token, k -> ConcurrentHashMap.newKeySet()).add(docId);
}
}
public Map<String, Set<Long>> getInvertedIndex() {
return invertedIndex;
}
}
사용자가 검색어를 입력하면, 검색 엔진은 이를 처리하여 관련된 문서를 찾아 반환합니다. 이 과정에서는 사용자의 쿼리를 분석하고, 인덱스에서 관련 문서를 검색합니다.
구현 시 고려 사항
@Service
public class SearchService {
@Autowired
private IndexingService indexingService;
public Set<Long> search(String query) {
String[] tokens = query.toLowerCase().split("\\W+");
Set<Long> result = new HashSet<>();
for(String token : tokens) {
Set<Long> docs = indexingService.getInvertedIndex().get(token);
if(docs != null) {
result.addAll(docs);
}
}
return result;
}
}
검색 결과를 단순히 관련 문서로 나열하는 것보다는, 사용자에게 가장 유용한 문서를 먼저 보여주는 것이 중요합니다. 이를 위해 문서의 중요도나 관련성을 평가하여 순위를 매깁니다.
구현 시 고려 사항
@Service
public class RankingService {
public double calculateTFIDF(String term, int termFreq, int docFreq, int totalDocs) {
double tf = 1 + Math.log(termFreq);
double idf = Math.log((double) totalDocs / (1 + docFreq));
return tf * idf;
}
public List<Long> rankDocuments(Set<Long> docs, String query, int totalDocs) {
Map<Long, Double> docScores = new HashMap<>();
for(Long docId : docs) {
// 각 문서에 대해 TF-IDF 점수 계산
double score = 0.0;
// 예: query의 각 단어에 대해 점수를 더함
docScores.put(docId, score);
}
return docScores.entrySet().stream()
.sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
검색 엔진은 대량의 데이터를 처리하고, 빠른 응답 속도를 유지해야 합니다. 따라서 인프라 설계와 확장성이 중요합니다.
구현 시 고려 사항
ex)
신입 Java/Spring 백엔드 개발자로서 검색 엔진 설계에 대한 이해를 높이고, 포트폴리오에 추가할 만한 실습 프로젝트를 통해 실력을 향상시키는 것은 매우 좋은 접근 방식입니다. 아래에서는 단계별로 진행할 수 있는 실습 프로젝트 아이디어와 그에 따른 구현 방법을 제안드립니다. 각 프로젝트는 Java와 Spring을 중심으로 구성되었으며, 검색 엔진의 주요 구성 요소를 직접 구현해보며 학습할 수 있습니다.
웹 페이지를 자동으로 탐색하고 데이터를 수집하는 크롤러를 구현하여, 크롤링의 기본 개념과 HTTP 요청 처리 방법을 익힙니다.
프로젝트 설정:
Spring Initializr 사용 권장).Spring Web, Jsoup (HTML 파싱 라이브러리).기본 크롤링 로직 구현:
Jsoup을 사용하여 웹 페이지에 HTTP 요청을 보내고 HTML을 파싱합니다.크롤링 제어:
Set<String>을 사용하여 중복 방문을 방지합니다.Queue<String>)를 사용하여 크롤링할 URL 목록을 관리합니다.멀티스레딩 도입:
@Async를 활용하여 비동기적으로 크롤링 작업을 수행합니다.Executor를 설정하여 스레드 풀을 관리합니다.// CrawlerService.java
@Service
public class CrawlerService {
private Set<String> visited = ConcurrentHashMap.newKeySet();
private Queue<String> queue = new ConcurrentLinkedQueue<>();
@Async
public void startCrawling(String startUrl) {
queue.add(startUrl);
while (!queue.isEmpty() && visited.size() < 100) { // 예: 최대 100 페이지 크롤링
String url = queue.poll();
if (url == null || visited.contains(url)) continue;
visited.add(url);
try {
Document doc = Jsoup.connect(url).get();
// 링크 추출
Elements links = doc.select("a[href]");
for (Element link : links) {
String nextUrl = link.absUrl("href");
if (!visited.contains(nextUrl)) {
queue.add(nextUrl);
}
}
// 페이지 내용 저장 또는 처리
System.out.println("Crawled: " + url);
} catch (IOException e) {
System.err.println("Failed to crawl: " + url);
}
}
}
public Set<String> getVisitedUrls() {
return visited;
}
}
// CrawlerController.java
@RestController
@RequestMapping("/crawler")
public class CrawlerController {
@Autowired
private CrawlerService crawlerService;
@PostMapping("/start")
public ResponseEntity<String> startCrawling(@RequestParam String startUrl) {
crawlerService.startCrawling(startUrl);
return ResponseEntity.ok("Crawling started.");
}
@GetMapping("/visited")
public ResponseEntity<Set<String>> getVisitedUrls() {
return ResponseEntity.ok(crawlerService.getVisitedUrls());
}
}
robots.txt 파일을 확인하여 크롤링 정책을 준수하세요.크롤링한 데이터를 효율적으로 검색할 수 있도록 인덱스를 생성하고 저장하는 시스템을 구현합니다.
데이터 모델링:
id, url, content).Map<String, Set<Long>> 형태로 단어와 문서 ID 매핑).인덱싱 로직 구현:
데이터 저장:
// Document.java
@Entity
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String url;
@Lob
private String content;
// getters and setters
}
// IndexingService.java
@Service
public class IndexingService {
private Map<String, Set<Long>> invertedIndex = new ConcurrentHashMap<>();
@Autowired
private DocumentRepository documentRepository;
public void indexDocument(Long docId, String content) {
String[] tokens = content.toLowerCase().split("\\W+");
for (String token : tokens) {
if (token.isEmpty() || isStopWord(token)) continue;
invertedIndex.computeIfAbsent(token, k -> ConcurrentHashMap.newKeySet()).add(docId);
}
}
private boolean isStopWord(String word) {
// 간단한 불용어 목록
List<String> stopWords = Arrays.asList("the", "is", "at", "which", "on", "and");
return stopWords.contains(word);
}
public Set<Long> search(String term) {
return invertedIndex.getOrDefault(term.toLowerCase(), Collections.emptySet());
}
}
// DocumentRepository.java
public interface DocumentRepository extends JpaRepository<Document, Long> {
}
사용자가 검색어를 입력하면 관련 문서를 반환하는 RESTful API를 구현하여 검색 기능을 체험합니다.
검색 API 엔드포인트 설계:
GET /search?query=검색어와 같은 엔드포인트를 설계합니다.검색 로직 구현:
결과 정렬 및 페이징:
// SearchController.java
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private IndexingService indexingService;
@Autowired
private DocumentRepository documentRepository;
@GetMapping
public ResponseEntity<List<Document>> search(@RequestParam String query) {
String[] terms = query.toLowerCase().split("\\W+");
Set<Long> resultIds = new HashSet<>();
for (String term : terms) {
resultIds.addAll(indexingService.search(term));
}
List<Document> results = documentRepository.findAllById(resultIds);
return ResponseEntity.ok(results);
}
}
검색 결과의 관련성을 높이기 위해 TF-IDF와 같은 랭킹 알고리즘을 적용하여 결과를 정렬합니다.
TF-IDF 계산 로직 구현:
랭킹 서비스 구현:
검색 결과에 랭킹 적용:
// RankingService.java
@Service
public class RankingService {
@Autowired
private DocumentRepository documentRepository;
@Autowired
private IndexingService indexingService;
public List<Document> rankDocuments(String query, Set<Long> docIds) {
String[] terms = query.toLowerCase().split("\\W+");
int totalDocs = documentRepository.count();
Map<Long, Double> docScores = new HashMap<>();
for (Long docId : docIds) {
double score = 0.0;
Document doc = documentRepository.findById(docId).orElse(null);
if (doc == null) continue;
String content = doc.getContent().toLowerCase();
for (String term : terms) {
int tf = countTermFrequency(content, term);
int df = indexingService.getDocumentFrequency(term);
double idf = Math.log((double) totalDocs / (1 + df));
score += (1 + Math.log(tf)) * idf;
}
docScores.put(docId, score);
}
return docScores.entrySet().stream()
.sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
.map(entry -> documentRepository.findById(entry.getKey()).get())
.collect(Collectors.toList());
}
private int countTermFrequency(String content, String term) {
return (int) Arrays.stream(content.split("\\W+"))
.filter(word -> word.equals(term))
.count();
}
}
// Updated SearchController.java
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private IndexingService indexingService;
@Autowired
private DocumentRepository documentRepository;
@Autowired
private RankingService rankingService;
@GetMapping
public ResponseEntity<List<Document>> search(@RequestParam String query) {
String[] terms = query.toLowerCase().split("\\W+");
Set<Long> resultIds = new HashSet<>();
for (String term : terms) {
resultIds.addAll(indexingService.search(term));
}
List<Document> rankedResults = rankingService.rankDocuments(query, resultIds);
return ResponseEntity.ok(rankedResults);
}
}
강력한 검색 기능을 제공하는 오픈소스 검색 엔진인 Elasticsearch를 Spring 애플리케이션에 통합하여, 실제 검색 엔진의 성능과 기능을 체험합니다.
Elasticsearch 설치 및 설정:
Spring Data Elasticsearch 연동:
spring-boot-starter-data-elasticsearch 의존성을 추가합니다.application.properties 또는 application.yml 파일에 추가합니다.데이터 인덱싱:
검색 API 구현:
<!-- pom.xml -->
<dependencies>
<!-- Other dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
# application.yml
spring:
elasticsearch:
client:
rest:
uris: http://localhost:9200
index:
name: documents
// ElasticsearchDocument.java
@Document(indexName = "documents")
public class ElasticsearchDocument {
@Id
private String id;
private String url;
private String content;
// getters and setters
}
// ElasticsearchRepository.java
public interface ElasticsearchRepository extends ElasticsearchRepository<ElasticsearchDocument, String> {
List<ElasticsearchDocument> findByContentContaining(String content);
}
// SearchController.java
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private ElasticsearchRepository elasticsearchRepository;
@GetMapping
public ResponseEntity<List<ElasticsearchDocument>> search(@RequestParam String query) {
List<ElasticsearchDocument> results = elasticsearchRepository.findByContentContaining(query);
return ResponseEntity.ok(results);
}
}
앞서 구현한 각 구성 요소(크롤링, 인덱싱, 검색, 랭킹)를 통합하여 간단한 검색 엔진을 완성합니다. 이 프로젝트는 포트폴리오에 추가할 수 있는 완성도 높은 프로젝트로, 다양한 기술을 체험할 수 있습니다.
프로젝트 구조 설계:
크롤러와 인덱서 통합:
검색 API 확장:
추가 기능 구현:
배포 및 운영:
Java & Spring:
웹 크롤링:
검색 엔진 이론:
Elasticsearch:
검색 엔진 설계와 구현은 다양한 백엔드 기술과 알고리즘을 체험할 수 있는 훌륭한 실습 주제입니다. 위에서 제안한 프로젝트를 통해 크롤링, 인덱싱, 검색, 랭킹 등 검색 엔진의 주요 구성 요소를 직접 구현해보며, Java와 Spring을 활용한 백엔드 개발 능력을 향상시킬 수 있습니다. 각 단계에서 발생하는 문제를 해결하고, 기능을 확장해 나가면서 실력을 키우는 동시에, 포트폴리오에 추가할 수 있는 의미 있는 프로젝트를 완성해보세요. 성공적인 취업 준비를 응원합니다!