Перевел статью по индексированию и поиску с использование Lucene API, работающие примеры на Java.
автор Erik Hatcher (01/27/2005) источник
перевод Сергей Лукин
По мере усложнения восприятия мира люди изобретали различные виды классификации, каталоголизации по различным иерархическим схемам (как то по видам и родам животных, по направлениям музыки в каталоге дисков). Развитие интернета и электронных хранилищ позволили сделать огромный шаг в построение сложных каталогов для быстрого поиска информации. Как пример, Yahoo содержит огромный классификатор сайтов интернета. Но поиск по каталогу долог и требует от человека хорошего знания предметной области, поэтому требовался «свободный» поиск, сквозной поиск по всему каталогу, с произвольно составленным запросом. Лучший пример этого демонстрирует Google.
Если же от вашего приложения пользователи требуют такой функциональности, то Lucene это лучший способ сделать это быстро, просто и эффективно.
Lucene это высокопроизводительный, масштабируемый, поисковый движок. В нем одновременно реализованы и функции индексирования и функции поиска, доступ к этим функциям предоставляется через API Lucene. В первой части этой статья вы увидите пример использования Lucene для индексирования файлов в каталоге и подкаталогах. Далее небольшое отступление описывающее формат индексного каталога Lucene. Потом о краткое исследования методов анализа текста и, наконец реализацию поиска.
Индексирование
В начале создадим класс Indexer который будем использовать для индексирования всех текстовых файлов в указанной директории. Этот утилитарный класс с единственным внешним (public) методом index(), который принимает два аргумента. Первый indexDir — объект класса File который ссылается на каталог в котором будет создаватся индексная база. Второй аргумент это другой объект класса File который в свою очередь ссылается на каталог который будет индексироваться.
public static void index(File indexDir, File dataDir) throws IOException {
if (!dataDir.exists() || !dataDir.isDirectory()) {
throw new IOException(dataDir + " does not exist or is not a directory");
}
IndexWriter writer = new IndexWriter(indexDir, new StandardAnalyzer(), true);
indexDirectory(writer, dataDir);
writer.close();
}
После проверки что dataDir существует и это каталог, мы инстанцируем объект класса IndexWriter который будет использоваться для создания индекса. Конструктор IndexWriter принимает в качестве первого параметра каталог в котором индексная база будет создана последний аргумент указывает что мы будем пересоздавать индекс, а не дополнять существующий (если таковой есть в каталоге). Средний параметр — это анализатор, который используется для анализа полей. Анализ полей будет описан ниже, но сейчас мы можем быть уверенны что все важные слова в файлах будут проиндексированы благодаря анализатору StandardAnalyzer.
Метод indexDirectory() ходит по дереву каталогов, сканирует его на наличие файлов с расширением .txt. Любой .txt файл будет проиндексирован методом indexFile(), а любой каталог будет обработан методом indexDirectory(), все остальные файлы будут проигнорированы. Ниже приведен код метода indexDirectory
private static void indexDirectory(IndexWriter writer, File dir) throws IOException
{
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
if (f.isDirectory()) {
indexDirectory(writer, f); // recurse
} else if (f.getName().endsWith(".txt")) {
indexFile(writer, f);
}
}
}
Метод indexDirectory() живет полностью независимо от Lucene. Этот пример использования Lucene основной — крайне редко при использовании Lucene приходиться много программировать с Lucene API, надо просто использовать готовое. В завершени класса Indexer, мы реализуем самое главное — индексирование отдельного текстового файла:
private static void indexFile(IndexWriter writer, File f) throws IOException {
System.out.println("Indexing " + f.getName());
Document doc = new Document();
doc.add(Field.Text("contents", new FileReader(f)));
doc.add(Field.Keyword("filename", f.getCanonicalPath()));
writer.addDocument(doc);
}
И… поверите вы или нет, но мы сделали это!
Мы проиндексировали все текстовые файлы в дереве каталогов.
Это действительно так просто. Подведем итог, перечислим шаги которые мы предприняли:
все файлы с расширением .txt)
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;import java.io.*;
public class Indexer {
public static void index(File indexDir, File dataDir) throws IOException {
if (!dataDir.exists() || !dataDir.isDirectory()) {
throw new IOException(dataDir
+ " does not exist or is not a directory");
}
IndexWriter writer = new IndexWriter(indexDir, new StandardAnalyzer(),
true);
indexDirectory(writer, dataDir);
writer.close();
}
private static void indexDirectory(IndexWriter writer, File dir)
throws IOException {
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
if (f.isDirectory()) {
indexDirectory(writer, f); // recurse
} else if (f.getName().endsWith(".txt")) {
indexFile(writer, f);
}
}
}
private static void indexFile(IndexWriter writer, File f)
throws IOException {
System.out.println("Indexing " + f.getName());
Document doc = new Document();
doc.add(Field.Text("contents", new FileReader(f)));
doc.add(Field.Keyword("filename", f.getCanonicalPath()));
writer.addDocument(doc);
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new Exception(
"Usage: " + Indexer.class.getName() + " < index > < data >");
}
File indexDir = new File(args[0]);
File dataDir = new File(args[1]);
index(indexDir, dataDir);
}
}
В этом примере в каждый документ входит два поля: contents — содержание текстового файла и его полное имя (с путем) — filename. Поле с содержанием специальным образом обрабатывается с помощью StandardAnalyzer, это будет описано позже. Поле filename индексируется как есть. Статические методы класса Filed: Text и Keywords будут подробно объяснены после краткого взгляда внутрь Lucene.
Внутри Lucene индекса
Формат индекса Lucene — это каталог с различными файлами. Вы можете успешно использовать Lucene и без понимания структуры каталога. Можно свободно пропустить этот раздел и рассматривать каталог как черный ящик без заботы о том что внутри. Если же вы готовы рассматривать глубже, то вы обнаружите что файлы созданные в прошлом разделе содержат статистику и другие данные облегчающие быстрый поиск и ранжирование. Индекс содержит последовательность документов. В нашем примере, каждый документ представляет информацию о текстовом файле.
Документ (Document
)
Документ это основный объект, которым оперирует Lucene. Документы содержат последовательность полей Поля имеют имена («contents» и «filename» в нашем примере).Значения полей это последовательность элементов (terms).
Элементы (Terms
)
Элементы это мельчайшие части конкретного поля. Поля имеют три атрибута.
Информация размеченных полей хранится очень эффективно, так как одни и те же элементы в тех же полях для множества документов сохраняются только единожды, с указанием на документ что содержит их.Класса Field имеет несколько статических методов для создания полей с различной комбинацией этих атрибутов. Вот они:
Field.Keyword
— Индексирован и сохранен, но не размеченный. Ключевые поляобычно используются для таких данных как имена файлов, номера партий, первичные ключи,
и другой текст который необходимо оставить незатронутым.
Field.Text
— Индексирован и размечен. Этот текст такжесохранен есть он добавлен как класс
String
, и не будет сохранен если добавлен как класс Reader.Field.UnIndexed
— Только сохраняется. По таким поля нельзя искать.Field.UnStored
— Индексирован и размечен, но не сохранен. Такие поля идеальны, если вы хотите искать по этому полю, но хранить его отдельно от индексной базы и нет необходимости получать оригинальный текст сразу при выполнении поискового запроса.Глядя на выше сказанное, Lucene кажется относительно простым. Но он просто внешне, внутренняя реализация сложна, и основная сложность заключена в методах анализа текста, и в том как хранить элементы из размеченных полей.Анализ
Анализ происходит в полях что помечены как размечаемые(tokenized
). В нашем примере, мы индексируем поле contents — содержание текстовых файлов. Наша цель сделать все слова в текстовом файле пригодными для поиска,
но на практике цель немного отличается нам не нужно делать индекс чувствительным к каждому полю. Такие слова как «a», «and» и «the» обычно обдуманно не подходят для поиска и исключением их из индекса можно оптимизировать
индекс, такие слова называется стоп-слова.
Какую сущность мы ищем? Что является словом, как отделить одно слово от другого. Акронимы, электронные адреса, интернет ссылки и другие такие конструкции, оставлять незатронутым и делать доступными для поиска? Если
слово в единственном числе проиндексировано, делать ли доступным для поиска его множественное число?
Это все интересные и сложные вопросы, ответы на которые позволяют выбрать какой из анализаторов использовать, или создавать свой.
В нашем примере, мы используем в встроенный в Lucene анализатор StandardAnalyzer
, но существуют и другие встроенные анализаторы, а также и некоторые дополнительные. Приведем не большой кусочек кода который позволяет рассмотреть работу различных анализаторов на двух различных строках.
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.analysis.snowball.SnowballAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import java.io.StringReader;
import java.io.IOException;public class AnalysisDemo {
private static final String[] strings = {
"The quick brown fox jumped over the lazy dogs",
"XY&Z Corporation - xyz@example.com" };
private static final Analyzer[] analyzers = new Analyzer[]{
new WhitespaceAnalyzer(),
new SimpleAnalyzer(),
new StopAnalyzer(),
new StandardAnalyzer(),
new SnowballAnalyzer("English", StopAnalyzer.ENGLISH_STOP_WORDS)
};
public static void main(String[] args) throws IOException {
for (int i = 0; i < strings.length; i++) {
analyze(strings[i]);
}
}
private static void analyze(String text) throws IOException {
System.out.println("Analzying \"" + text + "\"");
for (int i = 0; i < analyzers.length; i++) {
Analyzer analyzer = analyzers[i];
System.out.println("\t" + analyzer.getClass().getName() + ":");
System.out.print("\t\t");
TokenStream stream = analyzer.tokenStream("contents",
new StringReader(text));
new StringReader(text));
while (true) {
Token token = stream.next();
if (token == null) break;
System.out.print("[" + token.termText() + "] ");
}
System.out.println("\n");
}
}
}
Метод analyze использует исследовательскую форму Lucene API, для обычного индексирования эти функции не нужны, но удобны для рассмотрения как различные анализаторы размечают текст на элементы (термы). Результат выполнения следующий:
Analzying "The quick brown fox jumped over the lazy dogs"org.apache.lucene.analysis.WhitespaceAnalyzer:[The] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dogs] org.apache.lucene.analysis.SimpleAnalyzer: [the] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dogs] org.apache.lucene.analysis.StopAnalyzer: [quick] [brown] [fox] [jumped] [over] [lazy] [dogs] org.apache.lucene.analysis.standard.StandardAnalyzer: [quick] [brown] [fox] [jumped] [over] [lazy] [dogs] org.apache.lucene.analysis.snowball.SnowballAnalyzer: [quick] [brown] [fox] [jump] [over] [lazi] [dog] Analzying "XY&Z Corporation - xyz@example.com" org.apache.lucene.analysis.WhitespaceAnalyzer: [XY&Z] [Corporation] [-] [xyz@example.com] org.apache.lucene.analysis.SimpleAnalyzer: [xy] [z] [corporation] [xyz] [example] [com] org.apache.lucene.analysis.StopAnalyzer: [xy] [z] [corporation] [xyz] [example] [com] org.apache.lucene.analysis.standard.StandardAnalyzer: [xy&z] [corporation] [xyz@example] [com] org.apache.lucene.analysis.snowball.SnowballAnalyzer: [xy&z] [corpor] [xyz@exampl] [com]
Анализатор WhitespaceAnalyzer
самый простой, он просто разбивает на элементы основываясь на пробелах.
Он даже не меняет регистр букв. Поиск регистрочувствительный поэтому обычной практикой приведение к нижнему регистру текста в процессе фазы анализа. Остальные анализаторы приводят к нижнему регистру в процессе анализа. SimpleAnalyzer
разбивает текс основываясь на несимвольных разделителях, таких как специальные символы (‘&’, ‘@’, и ‘.’). StopAnalyzer
использует фенкциональность SimpleAnalyzer
и так же удаляет основные английские стоп-слова.
Наиболее сложный анализатор встроенный в ядро Lucene это StandardAnalyzer
. Под ним скрывается основанный на JavaCC парсер с правилами для электронных адресов, акронимов, веб-адресов, дробных чисел, а так же он приводит к нижнему регистру и удаляет слова входящие в список стоп-слов. Анализатор построен по архитектуре цепочки фильтров, таким образом несколько разноцелевых правил комбинируются.
Анализатор SnowballAnalyzer
демонстрирует не встроенный на данных момент в Lucene функциональность. Эта часть кода доступна в jakarta-lucene-sandbox CVS хранилище. Он показывает наиболее специфичные результаты. Алгоритм его основан на языке текста, и использует стемминг (stemming). Алгоритмы стемминга пытаются привести слово к его основной корневой форме. Это мы видим в примере с «lazy» который был приведен к «lazi». Слово «laziness» так же
будет приведено к «lazi», тем самым при поиск мы найдем оба документа сразу. Другой интересный пример работы
SnowballAnalzyer
с текстом «corporate corporation corporations corpse», который приведет к следующим результатам
[corpor] [corpor] [corpor] [corps]
На этом мы остановим исследование текстовых анализаторов. Так как за этой темой стоит множество диссертаций и патентов, и конечно множество исследований. Теперь, зная как разбивается текст при индексировании, построим класс для поиска.
Поиск
В соответствии с нашим примером индексирования, создадим класс поиска Seacher который показывает результаты для того же индекса. Основные части этого класса:
import org.apache.lucene.document.Document;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Hits;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import java.io.File;public class Searcher {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new Exception(
"Usage: " + Searcher.class.getName() + " < index > <query>");
}
File indexDir = new File(args[0]);
String q = args[1];
if (!indexDir.exists() || !indexDir.isDirectory()) {
throw new Exception(indexDir + " is does not exist or is not a directory.");
}
search(indexDir, q);
}
public static void search(File indexDir, String q) throws Exception{
Directory fsDir = FSDirectory.getDirectory(indexDir, false);
IndexSearcher is = new IndexSearcher(fsDir);
Query query = QueryParser.parse(q, "contents", new StandardAnalyzer());
Hits hits = is.search(query);
System.out.println("Found "
hits.length() + " document(s) that matched query '" + q + "':");
for (int i = 0; i < hits.length(); i++) {
Document doc = hits.doc(i);
System.out.println(doc.get("filename"));
}
}
}
Объект Query из API Lucene создается в IndexSearcher.search методе. Объект Query может быть создан через
API используя встроенные подклассы Query:
и несколько других. В нашем случае мы используем метод parse класса QueryParser для разбора введенного пользователем запроса. QueryParser это сложный основанный на JavaCC парсер который разбирает из запроса, похожего на Google запрос, в представление Lucene API Query.
Синтаксис запросов Lucene документирован на сайте Lucene, выражения могут содержать логические операции, указание на поля в которых искать, группировку, ранжирование запросов и многое другое. Как пример, выражение «+java -microsoft», которое вернет совпадения для документов содержащих слово «java», но не содержащих слово «microsoft.» QueryParser.parse необходимо указать поле по умолчанию для поиска, и в нашем случае мы укажем «contents» поле. Это будет эквивалентно запросу «+contents:java — contents:microsoft», но будет намного удобнее в использовании пользователям.
Так же разработчик должен указать какой анализатор использовать для разметки запроса. В нашем случае мы используем StandardAnalyzer, который тот же самый что и при индексировании. Обычно один и тот же анализатор используется и для индексирования и для разбора строки запроса. Если мы использовали SnowballAnalyzer то как было показано в примере исследований анализатора, то запрос со словом «laziness» вернет все документы с элементом «lazi».После поиска, возвращается набор ссылок на результаты — Hints Collection.
Ссылки возвращаются в порядке определяемый «очками» — релевантностью документа. Обсуждение алгоритма начисления очков не входит в рамки этой статьи, но можно быть уверенным что алгоритм по умолчанию подойдет к большинству приложений, и существует возможность настройки его в тех редких случаях когда этого алгоритма недостаточно.
Набор результатов сам по себе не является набором документов которые были найдены. Так сделано в частности и для повышения эффективности. Но ссылка (hint) представляет простой метод для получения документа. В нашем примере
мы выводим на экран поле с именем «filename» для каждого документа что были получены в результатах поиска.
Подводя итоги
Lucene логичный и красиво построенный продукт с изумительной функциональностью, это требует от разработчика искусного подхода к построению приложения вокруг него. Мы кратко обсудили проблему выбора анализатора, кроме этого
перед разработчиком стоят следующие вопросы:
демонстрируя как наиболее просто использовать Lucene.[от переводчика] В этой статье не рассмотрены такие интересные вопросы как:
Источники
Для дополнительной информации о Lucene, посетите сайт Lucene.
Там вы сможете найти информацию о синтаксисе запросов и формате индексных файлов.
Erik Hatcher соавтор книги об Ant’е,
«Java Development with Ant» (опубликовано издательством Manning),
и так же соавтор книги «Lucene in Action».