10.09.2012

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 а затем индексировать.

Комментариев нет:

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