Наша цель - разработать класс поискового движка, который способен быстро находить указанное слово среди pdf-файлов, причём ранжировать результаты по количеству вхождений. Также у нас будет сервер, который обслуживает входящие запросы с помощью этого движка. Но обо всём по-порядку.
Сделайте Fork этого репозитория в свой гитхаб-аккаунт через кнопку слева вверху:
Склонируйте форкнутый репозиторий и откройте его в идее как мавен-проект.
Помимо стандартных папок и файлов, в папке вашего проекта будет папка pdfs
- в ней находятся .pdf-файлы, по которым будет искать поисковый движок.
В проекте есть заготвки кода:
Класс | Описание |
---|---|
Main |
Сейчас в нём находится заготовка использования поискового движка. После его реализации, содержимое 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
и сохраняете в мапу в поле. Учтите также, что мы хотим регистронезависимый поиск, т.е. по слову "бизнес" должны учитываться и "бизнес", и "Бизнес" в документах (для этого при обработке достаточно каждое слово переводить в нижний регистр с помощью встроенного метода класса String
для этих целей).
Для подсчёта частоты слов можно использовать следующий приём (если не очень понятно как он работает, то можете его вынести в отдельный Main
и проанализировать отладчиком):
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
.
После того как вы это реализуете, можете протестировать работу вашего движка в Main
. Протестируйте на тех словах, на которых хотите. Вот пример работы на слове "бизнес":
[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-текста (библиотеку для работы с JSON подключите к pom.xml
).
Напоминалка как выглядит простой сервер
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
в нём должен теперь содержать суммарное количество раз, которое встретилось любое из слов запроса.
При этом слова из списка стоп-слов должны никак не влиять на запрос (т.е. игнорироваться), тк их встречаемость в запросе никакой информационной нагрузки не несёт (в нашем булевом поиске).
Тесты писать не нужно. Прикрепите ссылку на ваш публичный гит-репо с решением.