Lucene java

Apache Lucene набор библиотек java при помощи которых можно организовать полноценный поиск в интересующих вас данных(текстовых данных). На данный момент существует несколько поисковых движков использующих api Lucene таких как Apache Solr, Nutch, Hibernate Search. Без основ трудно разобраться и настроить их (движки), как бы вам не хотелось нужна база, основа понимания Lucene. Эту статью я хочу посвятить основам поиска на Lucene и так начнем наше погружение. Начнем как не странно с теории. Пусть у нас есть 2 книги:
Книга 1:
название первой книги - "Java in Action"
ISBN "1-932394-28-2"
год выпуска - "2011"

Книга 2:
название второй книги - "Java and Flex"
ISBN "1-932394-28-1"
год выпуска - "2010"

Давайте введем понятие Document. Document - это объект для поиска, который состоит из полей(Fields). В данном примере Книга 1, Книга 2 - это Документы, которые состоят из полей: Java in Action, 1-932394-28-2, 2011 и Java and Flex, 1-932394-28-1, 2010. Структура Lucene индекса такова что он содержит последовательность документов(Documents), котрые в свою очередь состоит из полей, поля имеют имена, количество и последовательность полей в каждом документе должна совпадать, а так же и их "тип данных", т. е. если в Документе первое поле содержит название книги, то и во втором Документе первое поле должно содержать название книги, а не год выпуска. Процесс поиска Lucene состоит в том что текстовое поле с помощью анализатора Lucene преобразуется в специальный индексный вид, terms и они сохраняются в индексе Lucene. Эти terms в дальнейшем используются для поиска в запросах по индексу Lucene. Важную роль при разбивки поля на terms играет анализатор, в зависимости от того какой анализатор вы выбрали вы получите те или иные terms. Анализатор преобразует текстовое поле обычно несколькими операциями, такими как извлечение "слова", сброс пунктуации (знаков припинания и т. д.), удаление частиц стоп-слов (stop words): in, a, the, at and e.t.c. приведение всех слов к нижнему регистру букв (lowercasing) - иногда называемую нормализацией (normalizing), удаление повторяющихся "слов", нахождение основы слова("корня") - стемминг (stemming), или получение из слова его нормальной/словарной формы - lemmatization. Процесс разбития текста по выше перечисленным правилам называется - tokenization, а сами части текста - tokens. Если кратко сказать term - это комбинация имени поля и token'a. Настало время закрепить всю теорию на практике и начнем все с анализаторов. Перед индексированием поле проходит через анализатор который разбивает поле на terms, анализаторы бывают разные и предназначены они для различных случаев. Перечислю некоторые из них:
WhitespaceAnalyzer - Разделяет на terms по пробелам.
SimpleAnalyzer - Производит деление текста по словам используя в качестве разделителей любые символы кроме букв, переводит в нижний регистр.
StopAnalyzer - Убирает стоп-слова(Естественно Английские :) ) и переводит текст в нижний регистр.
StandardAnalyzer - Анализирует текст, используя сложные грамматические правила. Переводит текст в нижний регистр и убирает стоп-слова.
EnglishAnalyzer и RussianAnalyzer (в предыдущих версиях носил название SnowballAnalyzer) - Переводит текст в нижний регистр, убирает стоп-слова и преобразует слово используя стемминг(stemming):
Расмотрим код он разбивает три строки
"The quick brown fox jumped over the LaZy dogs",
"Быстрая, рыжая лисица перепрыгнула через ленивых собак и быстро убежала",
"XY&Z Corporation - xyz@Example.com":

package org.vit;

import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.analysis.en.EnglishMinimalStemmer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.ru.RussianAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.util.Version;
import org.apache.lucene.util.AttributeSource;

import java.io.IOException;
import java.io.StringReader;


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(Version.LUCENE_31),
            new SimpleAnalyzer(Version.LUCENE_31),
            new EnglishAnalyzer(Version.LUCENE_31),
            new RussianAnalyzer(Version.LUCENE_31),
            new StopAnalyzer(Version.LUCENE_31),
            new StandardAnalyzer(Version.LUCENE_31)
    };

    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));
            while (true) {
                if (!stream.incrementToken()) break;
                AttributeSource token = stream.cloneAttributes();
                CharTermAttribute term =(CharTermAttribute) token.addAttribute(CharTermAttribute.class);
                System.out.print("[" + term.toString() + "] "); //2
            }
            System.out.println("\n");
        }
    }

} 

Результат работы программы:

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.en.EnglishAnalyzer:
		[quick] [brown] [fox] [jump] [over] [lazi] [dog] 

	org.apache.lucene.analysis.ru.RussianAnalyzer:
		[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] 

Analzying "Быстрая, рыжая лисица препрыгнула через ленивых собак и бысто убежала"
	org.apache.lucene.analysis.WhitespaceAnalyzer:
		[Быстрая,] [рыжая] [лисица] [препрыгнула] [через] [ленивых] [собак] [и] [бысто] [убежала] 

	org.apache.lucene.analysis.SimpleAnalyzer:
		[быстрая] [рыжая] [лисица] [препрыгнула] [через] [ленивых] [собак] [и] [бысто] [убежала] 

	org.apache.lucene.analysis.en.EnglishAnalyzer:
		[быстрая] [рыжая] [лисица] [препрыгнула] [через] [ленивых] [собак] [и] [бысто] [убежала] 

	org.apache.lucene.analysis.ru.RussianAnalyzer:
		[быстр] [рыж] [лисиц] [препрыгнул] [ленив] [собак] [быст] [убежа] 

	org.apache.lucene.analysis.StopAnalyzer:
		[быстрая] [рыжая] [лисица] [препрыгнула] [через] [ленивых] [собак] [и] [бысто] [убежала] 

	org.apache.lucene.analysis.standard.StandardAnalyzer:
		[быстрая] [рыжая] [лисица] [препрыгнула] [через] [ленивых] [собак] [и] [бысто] [убежала] 

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.en.EnglishAnalyzer:
		[xy] [z] [corpor] [xyz] [example.com] 

	org.apache.lucene.analysis.ru.RussianAnalyzer:
		[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]

Самый продвинутый StandardAnalyzer для английского текста но если копнуть глубже то любой анализатор это набор фильтров, так что для себя любимого можно написать свой анализатор если вас неустраивают стандартные. В lucene так же есть различные языковые анализаторы такие как
EnglishAnalyzer и RussianAnalyzer и для других языков (snowball) - котрые производят обрезку окончаний для своей локали (stemming). В принципе для поиска по индексу, нужно - первое создать индекс и второе написать програму поиска по индексу, чем мы и займемся. Прежде всего нужно выяснить что мы будем искать и где - работать будем с текстом, искать слова в тексте. Формирует Индекс объект класса IndexWriter - ему нужно указать где создавать индекс - в памяти, на диске, в БД; какой анализатор использовать, версию индекса, создать новый индекс или добавить в существующий. Далее для каждого документа, создать документ, в него добавить поля, сам документ добавить в объект класса IndexWriter, в конце оптимизировать индекс для более быстрого поиска. Давайте расмотрим пример:

package org.vit;

import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.ru.RussianAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.util.Version;

import java.io.IOException;
import java.io.File;

public class IndexerDemo {
    public static void main(String[] args){
        try {
            FSDirectory FSD =  FSDirectory.open(new File(".//Index"));  //индекс будем хранить в директории ./Index
            RussianAnalyzer analyzer = new  RussianAnalyzer(Version.LUCENE_31);  //какой используем анализатор
            IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_31,analyzer); //наш конфиг 
            iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE); //содаем всегда новый индекс
            IndexWriter writer = new IndexWriter(FSD, iwc); //создаем объект IndexWriter - или по другому индекс
            Document doc = new Document(); //создаем документ
            doc.add(new Field("id","1",Field.Store.YES,Field.Index.NOT_ANALYZED)); //добавляем 1-е поле в документ
            doc.add(new Field("name","Быстрая, рыжая лиса препрыгнула через ленивых собак и бысто убежала"
            ,Field.Store.YES,Field.Index.ANALYZED)); //добавляем 2-е поле в документ
            writer.addDocument(doc); //добавляем документ в индекс
            Document doc1 = new Document(); //повторяем тежe действия
            doc1.add(new Field("id","2",Field.Store.YES,Field.Index.NOT_ANALYZED));
            doc1.add(new Field("name","Рыжий лис",Field.Store.YES,Field.Index.ANALYZED));
            doc1.setBoost(10.0f);
            writer.addDocument(doc1);
            Document doc2 = new Document();
            doc2.add(new Field("id","3",Field.Store.YES,Field.Index.NOT_ANALYZED));
            doc2.add(new Field("name","Быстрая, рыжая лиса препрыгнула через ленивых собак и бысто убежала"
            ,Field.Store.YES,Field.Index.ANALYZED));
            writer.addDocument(doc2);
            Document doc3 = new Document();
            doc3.add(new Field("id","3",Field.Store.YES,Field.Index.NOT_ANALYZED));
            doc3.add(new Field("name","Рыжий лис",Field.Store.YES,Field.Index.NOT_ANALYZED));
            writer.addDocument(doc3);
            writer.optimize(); //оптимизируем индекс
            writer.close();  //все закрываем
            FSD.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Запустив программу вы получите lucene индекс в директории ./Index. Хочу обратить ваше внимание на еще одну важную деталь это как создаются поля:
new Field("name", - имя поля
"Рыжий лис" - текст поля
,Field.Store.YES, - текст поля будет сохраняться в индексе или нет Field.Store.NO
Field.Index.* - отвечает за то как будет индексироваться поле т.е. его текст возможные варианты:
NO - текст поля не индексируется, поле не может участвовать в поиске оно просто сохраняется используется как набор Field.Store.YES,Field.Index.NO, набор Field.Store.NO,Field.Index.NO - не допустим;
ANALYZED - текст поля пропускается через наш анализатор и получаем tokens которые потом сохраняются в индексе, поле участвует в поиске + расчитывается приоритет который в дальнейшем может быть использован при поиске;
NOT_ANALYZED - текст поля не пропускается через анализатор, весь текст считается одним единственным token который потом сохраняется в индексе, поле участвует в поиске + расчитывается приоритет который в дальнейшем может быть использован при поиске;
NOT_ANALYZED_NO_NORMS - для экспертов, текст поля не пропускается через анализатор, весь текст считается одним единственным token который потом сохраняется в индексе, поле участвует в поиске + еще у поля отключена фишка как приоритет при поиске;
ANALYZED_NO_NORMS - для экспертов, текст поля пропускается через наш анализатор и получаем tokens которые потом сохраняются в индексе, поле участвует в поиске + еще у поля отключена фишка как приоритет при поиске;
на счет приоритета по умолчанию он стоит 1.0f у всех полей, установить его можно setBoost(10.0f) и чем выше число тем выше приоритет при поиске.
Переходим к поиску, ищет в индексе - объект класса IndexSearcher, ему нужно указать где лежит индекс - в памяти, на диске, в БД; какой анализатор использовать/неиспользовать, версию индекса, поле поиска, что ищем (token). Расмотрим пример:

package org.vit;

import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.search.*;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.analysis.ru.RussianAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.util.Version;
import java.io.File;
import java.io.IOException;

public class SearchDemo {
    public static void main(String[] args) {
        try {
            Directory directory = FSDirectory.open(new File(".//Index")); //где находится индекс
            IndexSearcher is = new IndexSearcher(directory); //объект поиска
            QueryParser parser = new QueryParser(Version.LUCENE_31, "name", new RussianAnalyzer(Version.LUCENE_31));//поле поиска + анализатор  
            Query query = parser.parse("лиса"); //что ищем 
            TopDocs results =is.search(query, null, 10); //включаем поиск ограничиваемся 10 документами, results содержит ...
            System.out.println("getMaxScore()="+results.getMaxScore()+" totalHits="+results.totalHits); // MaxScore - наилучший результат(приоритет), totalHits - количество найденных документов
            for (ScoreDoc hits:results.scoreDocs) { // получаем подсказки
                Document doc = is.doc(hits.doc); //получаем документ по спец сылке doc
                System.out.println("doc="+hits.doc+" score="+hits.score);//выводим спец сылку doc + приоритет
                System.out.println(doc.get("id")+" | "+doc.get("name"));//выводим поля найденного документа 
            }
            directory.close();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

выполнив программу получаем результат:

getMaxScore()=7.0 totalHits=3 //наилучший приоритет 7.0 всего совпадений 3
doc=1 score=7.0 //внутреннея сылка lucene 1 приоритет 7.0
2 | Рыжий лис //поля найденного документа
doc=0 score=0.3125 //если приоритет совпадает 0.3125(doc=0) и 0.3125(doc=2) то сортируются результаты по doc
1 | Быстрая, рыжая лиса препрыгнула через ленивых собак и бысто убежала
doc=2 score=0.3125
3 | Быстрая, рыжая лиса препрыгнула через ленивых собак и бысто убежала 

Обратите внимание еще на одну вещь поиск не вывел документ doc3, при создании индекса мы указали характеристи поля так:
doc3.add(new Field("name","Рыжий лис",Field.Store.YES,Field.Index.NOT_ANALYZED)); так как при сохранении индекса было указано что поле не будет обрабатываться анализатором (Field.Index.NOT_ANALYZED) то и значение поля "Рыжий лис" - это один целый token, а так как мы ищем token "лис" то у нас не происходит совпадения этих token'ов, и документ не находится.
в программе использовал следующие библиотеки:
lucene-analyzers-3.1.0.jar
lucene-core-3.1.0.jar

PS lucene работает с текстом в кодировке UTF-8 если вам захочется проиндексировать текст допустим в кодировке windows-1251 то вам прежде нужно перевести его в UTF-8 а затем индексировать.

ВложениеРазмер
Lucene3Test.zip17.86 кб

Сильно! 

Сильно! 

Не осилил, много букв

Не осилил, много букв

Сортировка результатов

Мега статья) все бы так подробно и понятно писали. А как сортировать? в Lucene in Action жуткий пример, я вообще не понял

Регистр символов WhitespaceAnalyzer

А как же быть с регистром, на мой взгляд самый удобный это WhitespaceAnalyzer позволяет искать C++ TCP/IP и т.д но так понял что поиск регистр зависимый и как быть с результатами поиска?

Спасибо!

Спасибо! После прочтения Вашей статьи у меня в голове многое встало на свои места =)

спасибо

отличная статья, спасибо! все отлично разобрано!

Отправить комментарий

КАПЧА
Этот вопрос задается для того, чтобы выяснить, являетесь ли Вы человеком или представляете из себя автоматическую спам-рассылку.
CAPTCHA на основе изображений
Enter the characters shown in the image.