Класс поискового движка, который способен быстро находить указанное слово среди pdf-файлов, причём ранжировать результаты по количеству вхождений.
Сервер обслуживает входящие запросы с помощью этого движка.
В папке проекта есть папка pdfs
- в ней находятся .pdf-файлы, по которым будет искать поисковый движок.
Структура движка:
Класс | Описание |
---|---|
Main |
Запуск сервера, обслуживающего поисковые запросы |
PageEntry |
Класс, описывающий один элемент результата одного поиска. Он состоит из имени пдф-файла, номера страницы и количества раз, которое встретилось это слово на ней |
SearchEngine |
Интерфейс, описывающий поисковый движок. Всё что должен уметь делать поисковый движок, это на запрос из слова отвечать списком элементов результата ответа |
BooleanSearchEngine |
Реализация поискового движка, которую вам предстоит написать. Слово Boolean пришло из теории информационного поиска, тк наш движок будет искать в тексте ровно то слово, которое было указано, без использования синонимов и прочих приёмов нечёткого поиска |
Для работы с пдф использовается библиотека com.itextpdf:itext7-core:7.1.15
, которая подключена в pom.xml
:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.1.15</version>
<type>pom</type>
</dependency>
Основные инструменты из этой библиотеки:
- Чтобы создать объект пдф-документа, нужно указать объект
File
этого документа следующим классам:var doc = new PdfDocument(new PdfReader(pdf));
. - Чтобы получить объект одной страницы документа, нужно вызвать
doc.getPage(номерСтраницы)
. Полистайте методыdoc
чтобы найти способ узнать количество * страниц в документе. - Чтобы получить текст со страницы, используйте
var text = PdfTextExtractor.getTextFromPage(page);
. - Чтобы разбить текст на слова (а они в этих документах разделены могут быть не только пробелами), используйте
var words = text.split("\\P{IsAlphabetic}+");
.
Класс BooleanSearchEngine
: поиск (метод search
) должен работать быстро, поэтому предпочтём сканирование всех пдф-ок в конструкторе класса с сохранением информации для каждого слова из pdf-файлов. Тогда метод search
сможет отрабатывать быстро, по сути возвращать уже посчитанный в конструкторе для слова готовый список-ответ. Т.е. в конструкторе для каждого слова нужно сохранить готовый на возможный будущий запрос ответ в виде List<PageEntry>
Для этого можно использовать мапу, где ключом будет слово, а значением - искомый список. Такое предварительное сканирование файлов по которым мы будем искать называется индексацией.
В итоге, нужно реализовать логику индексации в конструкторе. Сканируя каждый пдф-файл класс перебираетего страницы, для каждой страницы извлекаются из неё слова и подсчитывается их количество. После подсчёта, для каждого уникального слова пдф-файла создаёте объект PageEntry
и сохраняется в мапу в поле. Поиск регистронезависимый.
Для подсчёта частоты слов используется следующий приём:
Map<String, Integer> freqs = new HashMap<>(); // мапа, где ключом будет слово, а значением - частота
for (var word : words) { // перебираем слова
if (word.isEmpty()) {
continue;
}
word = word.toLowerCase();
freqs.put(word, freqs.getOrDefault(word, 0) + 1);
}
Списки ответов для каждого слова отсортированы в порядке уменьшения поля count
. Для этого класс PageEntry
реализует интерфейс Comparable
.
Вот пример работы на слове "бизнес":
[PageEntry{pdf=Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf, page=12, count=6}, PageEntry{pdf=Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf, page=4, count=3}, PageEntry{pdf=Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf, page=5, count=3}, PageEntry{pdf=1. DevOps_MLops.pdf, page=5, count=2}, PageEntry{pdf=Что такое блокчейн.pdf, page=1, count=2}, PageEntry{pdf=Что такое блокчейн.pdf, page=3, count=2}, PageEntry{pdf=Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf, page=2, count=1}, PageEntry{pdf=Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf, page=11, count=1}, PageEntry{pdf=1. DevOps_MLops.pdf, page=3, count=1}, PageEntry{pdf=1. DevOps_MLops.pdf, page=4, count=1}, PageEntry{pdf=Что такое блокчейн.pdf, page=2, count=1}, PageEntry{pdf=Что такое блокчейн.pdf, page=4, count=1}, PageEntry{pdf=Что такое блокчейн.pdf, page=5, count=1}, PageEntry{pdf=Что такое блокчейн.pdf, page=7, count=1}, PageEntry{pdf=Что такое блокчейн.pdf, page=9, count=1}, PageEntry{pdf=Продвижение игр.pdf, page=7, count=1}, PageEntry{pdf=Как управлять рисками IT-проекта.pdf, page=2, count=1}]
В main
запускается сервер, слушающий порт 8989
, к которому происходят подключения и на входной поток подается одно слово (обозначим как word
), отвечать результатом вызова метода search(word)
, но в виде JSON-текста.
Простой сервер
try (ServerSocket serverSocket = new ServerSocket(8989);) { // стартуем сервер один(!) раз
while (true) { // в цикле(!) принимаем подключения
try (
Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream());
) {
// обработка одного подключения
}
}
} catch (IOException e) {
System.out.println("Не могу стартовать сервер");
e.printStackTrace();
}
Пример ответа на запрос:
[
{
"pdfName": "Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf",
"page": 12,
"count": 6
},
{
"pdfName": "Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf",
"page": 4,
"count": 3
},
{
"pdfName": "Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf",
"page": 5,
"count": 3
},
{
"pdfName": "1. DevOps_MLops.pdf",
"page": 5,
"count": 2
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 1,
"count": 2
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 3,
"count": 2
},
{
"pdfName": "Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf",
"page": 2,
"count": 1
},
{
"pdfName": "Этапы оценки проекта_ понятия, методы и полезные инструменты.pdf",
"page": 11,
"count": 1
},
{
"pdfName": "1. DevOps_MLops.pdf",
"page": 3,
"count": 1
},
{
"pdfName": "1. DevOps_MLops.pdf",
"page": 4,
"count": 1
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 2,
"count": 1
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 4,
"count": 1
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 5,
"count": 1
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 7,
"count": 1
},
{
"pdfName": "Что такое блокчейн.pdf",
"page": 9,
"count": 1
},
{
"pdfName": "Продвижение игр.pdf",
"page": 7,
"count": 1
},
{
"pdfName": "Как управлять рисками IT-проекта.pdf",
"page": 2,
"count": 1
}
]
Метод запроса теперь принимает не слово, а полноценный запрос из нескольких слов.
Всё также возвращается в качестве результата поиска список из PageEntry
, только count
в нём должен теперь содержать суммарное количество раз, которое встретилось любое из слов запроса.
При этом слова из списка стоп-слов должны никак не влиять на запрос (т.е. игнорироваться), тк их встречаемость в запросе никакой информационной нагрузки не несёт (в нашем булевом поиске).