Глава 14. Персистентность
В следующих нескольких упражнениях мы вернёмся к построению веб-поискового движка. Для обзора, компонентами поискового движка являются:
Если вы выполнили упражнение 8.3, вы реализовали индекс с помощью Java-отображения. В этом упражнении мы пересмотрим индексатор и создадим новую версию, которая будет хранить результаты в базе данных.
Если вы сделали упражнение 7.4, вы создали паука, который отслеживает найденную им первую ссылку. В следующем упражнении мы создадим более универсальную версию, которая сохранит каждую найденную ссылку в очереди и отсортирует их.
Затем, наконец, вы будете работать над проблемой поиска.
В этих упражнениях я предоставляю меньше исходного кода, и вы примете больше дизайнерских решений. Эти упражнения также более открыты. Я предложу несколько минимальных целей, которые вы должны попытаться достичь, но если вы хотите бросить себе вызов, есть много способов сделать это глубже.
Теперь давайте начнём писать новую версию индексатора.
14.1 Redis
Предыдущая версия индексатора хранила индекс в двух структурах данных: TermCounter сопоставлял поисковые слова с количеством их появления на веб-странице, а Index сопоставлял поисковые слова со списком страниц, на которых они появились.
Эти структуры данных хранятся в памяти работающей программы Java, что означает, что индекс теряется, когда программа завершает работу. Данные, хранящиеся только в памяти запущенной программы, называются «эфемерными», потому что они исчезают, когда программа завершается.
Данные, которые всё ещё существуют после завершения работы программы, называются «персистентными». Обычно данные, хранящиеся в файловой системе или в базе данных, являются персистентными.
Один из простых способов сделать данные персистентными — сохранить их в файле. Перед завершением работы он может преобразовать свои структуры данных в формат JSON (http://thinkdast.com/json) и записать их в файл. Когда он снова запустится, он сможет прочитать файл и восстановить структуру данных.
Но у этого решения есть несколько проблем:
Лучшим выбором является предоставление базы данных для постоянного хранения и возможность читать и записывать части базы данных без необходимости чтения и записи всего набора данных.
Существует множество систем управления базами данных (СУБД), которые предоставляют различные функции. Вы можете прочитать обзор на http://thinkdast.com/database.
Я рекомендую базу данных Redis для этого упражнения, которая предоставляет структуры данных, подобные структурам данных Java, для персистентного хранения. Конкретно, она предоставляет:
Переводчик: Также есть упорядоченные наборы, похожие на LinkedHashSet в Java.
Redis — это «база данных ключ-значение», что означает, что данные (значение), содержащиеся в ней, идентифицируются уникальным строковым ключом (ключом). Ключи в Redis похожи на ссылки в Java: они идентифицируют объект. Мы увидим несколько примеров позже.
14.2 Redis клиент и сервер
Обычно Redis работает как удалённый сервис; фактически его название представляет собой аббревиатуру от «REmote DIctionary Server» (удаленный словарный сервер, словарь — это, по сути, отображение). Чтобы использовать Redis, вам необходимо запустить сервер Redis где-нибудь и использовать клиент Redis для подключения к серверу Redis. Существует множество способов настроить сервер, и существует множество клиентов, которые можно использовать. Для этого упражнения я предлагаю:
Вот более подробные инструкции, чтобы помочь вам начать:
redis://redistogo:1234567feedfacebeefa1e1234567@dory.redistogo.com:10534
Этот URL содержит имя хоста сервера dory.redistogo.com
, номер порта 10534
и пароль, необходимый для подключения к серверу, который представляет собой длинную строку букв и цифр посередине. Вам понадобится эта информация для следующего шага.
14.3 Создание индекса на основе Redis
Вы найдёте исходный код для этого упражнения в репозитории книги:
Вам также понадобятся эти файлы, с которыми вы столкнулись в предыдущих упражнениях:
Если у вас есть рабочие версии этих файлов, вы можете использовать их для этого упражнения. Если у вас нет опыта работы с предыдущими упражнениями или вы не уверены в своём решении, вы можете скопировать моё решение из папки solutions.
Первый шаг — использовать Jedis для подключения к вашему серверу Redis. JedisMaker показывает, как это сделать. Он считывает информацию о вашем сервере Redis из файла, подключается к нему и входит в систему с вашим паролем, а затем возвращает объект Jedis, который можно использовать для выполнения операций Redis.
Если вы откроете JedisMaker.java, вы увидите класс JedisMaker, который является вспомогательным классом, предоставляющим статический метод make, который создаёт объект Jedis. Как только объект аутентифицирован, вы можете использовать его для связи с вашей базой данных Redis.
JedisMaker считывает информацию вашего сервера Redis из файла с именем redis_url.txt, который вы должны поместить в каталог src/resources:
redis://redistogo:1234567feedfacebeefa1e1234567@dory.redistogo.com:10534
Поскольку этот файл содержит пароль вашего сервера Redis, вы не должны помещать этот файл в общедоступный репозиторий. Чтобы помочь вам избежать случайного размещения, репозиторий содержит файл .gitignore, который затрудняет (но не делает невозможным) размещение файла в вашем репозитории.
Теперь запустите ant build для компиляции исходных файлов и ant JedisMaker для запуска примера кода в main:
public static void main(String[] args) {
Jedis jedis = make();
// String
jedis.set("mykey", "myvalue");
String value = jedis.get("mykey");
System.out.println("Got value: " + value);
// Set
jedis.sadd("myset", "element1", "element2", "element3");
System.out.println("element2 is member: " +
jedis.sismember("myset", "element2"));
// List
jedis.rpush("mylist", "element1", "element2", "element3");
System.out.println("element at index 1: " +
``` **Вот перевод текста на русский язык:**
Этот пример демонстрирует типы данных и методы, которые вы, скорее всего, будете использовать в этом упражнении. При его выполнении вывод должен быть следующим:
Получил значение: myvalue element2 является членом: true элемент с индексом 1: element2 частота слова1: 2 частота слова2: 1
В следующем разделе я объясню принцип работы кода.
## 14.4 Типы данных Redis
Redis по сути представляет собой отображение от ключа к значению, где ключ — это строка, а значение может быть строкой или одним из нескольких типов данных. Самый базовый тип данных в Redis — это строка. Я буду использовать курсив для обозначения типов Redis, чтобы отличать их от типов Java.
Чтобы добавить строку в базу данных, используйте `jedis.set`, аналогично `Map.put`; параметры — это новый ключ и соответствующее значение. Чтобы найти ключ и получить его значение, используйте `jedis.get`:
```java
jedis.set("mykey", "myvalue");
String value = jedis.get("mykey");
Здесь ключ — "mykey"
, а значение — "myvalue"
.
Redis предоставляет структуру коллекции, аналогичную Set<String>
в Java. Чтобы добавить элементы в коллекцию Redis, вы можете выбрать ключ для идентификации коллекции и затем использовать jedis.sadd
:
jedis.sadd("myset", "element1", "element2", "element3");
boolean flag = jedis.sismember("myset", "element2");
Вам не нужно выполнять отдельные шаги для создания коллекции. Если она не существует, Redis создаст её. В этом случае он создаст набор с именем myset
, содержащий три элемента.
Метод jedis.sismember
проверяет, является ли элемент членом коллекции. Добавление элементов и проверка членов являются операциями с постоянным временем.
Redis также предоставляет структуру списка, аналогичную List<String>
в Java. Метод jedis.rpush
добавляет элементы в конец (правый конец) списка:
jedis.rpush("mylist", "element1", "element2", "element3");
String element = jedis.lindex("mylist", 1);
Опять же, вам не нужно создавать структуру перед добавлением элементов. Этот пример создаёт список с именем mylist
, содержащий три элемента.
Метод jedis.lindex
использует целочисленный индекс и возвращает указанный элемент списка. Добавление и доступ к элементам являются операциями с постоянным временем.
Наконец, Redis предоставляет структуру хэша, аналогичную Map<String, String>
в Java. Метод jedis.hset
добавляет новую запись в хеш-таблицу:
jedis.hset("myhash", "word1", Integer.toString(2));
String value = jedis.hget("myhash", "word1");
Этот пример создаёт хеш-таблицу с именем myhash
, содержащую одну запись, которая сопоставляет ключ word1
со значением "2"
.
И ключи, и значения являются строками, поэтому если мы хотим сохранить Integer
, нам необходимо преобразовать его в String
перед вызовом hset
. Когда мы используем hget
для поиска значения, результатом будет String
, поэтому нам, возможно, придётся преобразовать его обратно в Integer
.
Использование хеш-таблицы Redis может сбивать с толку, поскольку мы используем один ключ для идентификации нужной хеш-таблицы, а затем другой ключ для идентификации значения в хеш-таблице. В контексте Redis второй ключ называется «поле», что может помочь сохранить ясность. Таким образом, ключ, подобный myhash
, обозначает конкретную хеш-таблицу, а поле, подобное word1
, идентифицирует значение в хеш-таблице.
Для многих приложений значения в хэш-таблицах Redis являются целыми числами, поэтому Redis предоставляет несколько специальных методов, таких как hincrby
, которые обрабатывают значения как числа:
jedis.hincrBy("myhash", "word2", 1);
Этот метод обращается к myhash
, получает текущее значение word2
(если оно не существует, то равно 0
), увеличивает его на 1
и записывает результат обратно в хеш-таблицу.
Установка, получение и увеличение записей в хеш-таблице являются операциями с постоянным временем.
Вы можете прочитать больше о типах данных Redis на сайте http://thinkdast.com/redistypes.
На данный момент вы можете получить некоторую информацию, которую вам нужно использовать для создания индекса поисковой системы, который будет хранить результаты в базе данных Redis.
Теперь запустите ant JedisIndexTest
. Он должен завершиться ошибкой, потому что у вас есть работа!
JedisIndexTest
тестирует следующие методы:
JedisIndex
, который является конструктором, принимающим объект Jedis
в качестве параметра.indexPage
, который добавляет веб-страницу в индекс; ему требуется StringURL
и объект jsoup Elements
, содержащий элементы страницы, для которых должен быть создан индекс.getCounts
, который принимает поисковый термин и возвращает Map<String, Integer>
, содержащее отображение поискового термина на количество его появлений на странице.Вот пример использования этих методов:
WikiFetcher wf = new WikiFetcher();
String url1 =
"http://en.wikipedia.org/wiki/Java_(programming_language)";
Elements paragraphs = wf.readWikipedia(url1);
Jedis jedis = JedisMaker.make();
JedisIndex index = new JedisIndex(jedis);
index.indexPage(url1, paragraphs);
Map<String, Integer> map = index.getCounts("the");
Если мы посмотрим на url1
в результате map
, мы должны получить 339
, что является количеством появлений the
на странице Java Википедии (то есть сохранённой нами версии).
Повторное индексирование той же страницы приведёт к замене старого результата новым.
Одним из советов по переводу структуры данных из Java в Redis является помнить, что каждый объект в базе данных Redis идентифицируется уникальным ключом, который является строкой. Если в одной базе данных есть два объекта, может потребоваться добавить префикс к ключу, чтобы различать их. Например, в нашем решении у нас есть два типа объектов:
URLSet
как набор Redis, содержащий URL
, где URL
содержит заданный поисковый термин. Каждый ключ URLSet
начинается с "URLSet:"
, поэтому для получения набора, содержащего слово the
, мы используем ключ "URLSet:the"
для доступа к этому набору.TermCounter
как хеш-таблицу Redis, которая отображает каждое слово, появляющееся на странице, на его количество появлений. Каждый ключ TermCounter
начинается с "TermCounter:"
, за которым следует URL страницы, которую мы ищем.В моей реализации каждый поисковый термин имеет свой собственный URLSet
, и каждая проиндексированная страница имеет свой собственный TermCounter
. Я предоставляю два вспомогательных метода, urlSetKey
и termCounterKey
, для сборки этих ключей.
К этому моменту у вас есть вся информация, необходимая для завершения упражнения, так что вы можете начать, если готовы. Однако у меня есть несколько советов, которые вы, возможно, захотите прочитать:
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )