Эта статья является частью нашего академического курса под названием Apache Lucene Fundamentals .
В этом курсе вы познакомитесь с Lucene. Вы поймете, почему такая библиотека важна, а затем узнаете, как работает поиск в Lucene. Кроме того, вы узнаете, как интегрировать Lucene Search в ваши собственные приложения, чтобы обеспечить надежные возможности поиска. Проверьте это здесь !
Содержание
1. Информационная перегрузка / взрыв
В настоящее время функциональность поиска в приложениях становится все более важной функцией. В конце концов, Интернет это все об информации, и все о получении информации в нужное время и с правой стороны.
Информационный взрыв происходит в форме быстрого увеличения количества опубликованной цифровой информации в нашем современном мире и последствий обилия этих необработанных и неструктурированных данных. Этот информационный взрыв привел к постоянной информационной перегрузке для всех нас.
Информационная перегрузка в настоящее время является распространенным явлением в офисах по всему миру. Некоторые из причин включают в себя:
- Широко распространенный доступ к сети
- Простота отправки сообщений электронной почты большому количеству людей
- Поскольку информация может дублироваться бесплатно, создание дополнительных копий не требует переменных затрат — люди отправляют отчеты и информацию людям, которые могут нуждаться в информации, а не в том, чтобы знать.
- Плохо созданные источники информации (особенно онлайн), которые:
- не упрощаются и не фильтруются, чтобы сделать их короче
- не написаны четко, поэтому люди должны тратить больше времени на их понимание
- содержать фактические ошибки или несоответствия — требующие дальнейшего исследования
Решение
Хотя не существует простого и единственного решения вышеуказанной проблемы, есть несколько способов, которые можно использовать для смягчения проблемы.
Они включают:
- Тратить меньше времени на получение информации, которую «приятно знать», и больше времени на то, что нам «нужно знать сейчас».
- Ориентация на качество информации, а не количество. Краткое сжатое электронное письмо более ценно, чем длинное.
- Научиться создавать лучшую информацию. Будьте прямолинейны в том, что мы просим людей, чтобы они могли дать точные ответы.
- Однозадачность и сосредоточенность ума на одной проблеме за раз.
Теперь, кроме этого, мы можем реализовать решение для поиска информации с помощью библиотеки поиска с открытым исходным кодом Apache Lucene, которая способна извлекать информацию из такого неструктурированного содержимого, если мы можем получать текстовые данные из хранилища содержимого.
Краткий обзор поискового приложения
- Хранить файлы в файловой системе.
- При хранении файлов нам нужно будет добавить файл в качестве документа в индекс Lucene.
- При удалении файлов нам нужно будет удалить запись файла из соответствующего индекса Lucene.
- Анализируйте документ с помощью стандартного анализатора Lucene (мы можем использовать несколько других анализаторов, которые подключаются к Lucene)
- Обновите индекс Lucene с помощью дополнительного поля, такого как путь к файлу в документе.
- Начать поиск с Lucene Standard Analysis
- Мы можем получить результат такого поиска с гораздо большей скоростью, чем поиск по реляционной базе данных для миллионов документов.
- Теперь, если у нас есть ссылка на файл в файловой системе в индексируемом хранилище Lucene, мы можем просмотреть его — что может быть одной из целей нашего приложения.
Вышеупомянутый вариант использования, конечно, не единственное решение для всех текстовых поисков и поиска информации из огромных информационных хранилищ. В некоторых случаях может быть достаточно простой функции поиска в базе данных. И требования обработки данных с другими инструментами, такими как Apache Hadoop, также являются приемлемыми вариантами.
2. Компонент для индексации неструктурированных данных
Компонент индексирования поддерживает каталог для файлов, которые доступны пользователю для извлечения файлов. Компонент индексирования является дополнительной функцией и должен быть установлен на любом сервере, к которому пользователи будут обращаться для извлечения файлов. Компонент индексирования поддерживает поиск по файлам, версиям файлов и недавним действиям.
Давайте рассмотрим некоторые термины, связанные с индексацией Lucene:
Индексируемый объект
Те файлы или информация, которые хранятся в хранилище индексов Lucene, называются индексированными объектами.
Каждый индекс Lucene управляется одним менеджером индекса, который уникально идентифицируется по имени. В большинстве случаев существует также отношение один к одному между индексируемым объектом и одним IndexManager
(управляет индексацией). Исключением являются случаи использования индексации и совместного использования индекса. Первый может быть применен, когда индекс для одного объекта становится слишком большим, а операции индексации замедляют работу приложения. В этом случае одна сущность индексируется в несколько индексов, каждый со своим собственным менеджером индексов. Последнее, совместное использование индекса, — это возможность индексировать несколько объектов в один и тот же индекс Lucene.
Индексы шардинга
В некоторых случаях может быть полезно разделить (осколить) индексированные данные данного объекта на несколько индексов Lucene.
Возможные варианты использования для шардинга:
- Один индекс настолько велик, что время обновления индекса замедляет работу приложения.
- Типичный поиск затрагивает только подмножество индекса, например, когда данные естественно сегментированы по клиенту, региону или приложению.
Совместное использование индексов
Технически возможно хранить информацию более чем одного объекта в одном индексе Lucene. Есть два способа сделать это:
- Конфигурирование базовых провайдеров каталогов, чтобы они указывали на один и тот же каталог физических индексов Мы должны использовать тот же индекс (каталог) для объекта «Мебель и животные». Мы просто устанавливаем
indexName
для обеих сущностей, например, «Animal». Оба объекта будут сохранены в каталоге Animal. - Установка
@Indexed
индекса аннотации@Indexed
для сущностей, которые вы хотите объединить, с тем же значением. Если бы мы снова хотели, чтобы все экземпляры Furniture были проиндексированы в индексе Animal вместе со всеми экземплярами Animal, мы указали бы@Indexed(index="Animal")
для классов Animal и Furniture.
3. Компоненты поиска данных
Основные классы индексации
Lucene может быстро получать ответы на запросы поиска, поскольку вместо поиска по тексту он выполняет поиск по индексу. Это будет эквивалентно поиску страниц в книге, связанной с ключевым словом, путем поиска по индексу в конце книги, а не по поиску слов на каждой странице книги.
Этот тип индекса называется инвертированным индексом, потому что он инвертирует структуру данных, ориентированную на страницы (page-> words), в структуру данных, ориентированную на ключевые слова (word-> pages).
Мы можем отсортировать основные классы индексации соответственно,
- IndexWriter
- каталог
- анализатор
- Документ
- поле
Для создания индекса первое, что нужно сделать, — это создать объект IndexWriter
. Объект IndexWriter
используется для создания индекса и добавления новых записей индекса (т. IndexWriter
Документов) в этот индекс. Вы можете создать IndexWriter
следующим образом:
1
|
IndexWriter indexWriter = new IndexWriter( "index-directory" , new StandardAnalyzer(), true ); |
Первый параметр указывает каталог, в котором будет создан индекс Lucene, в данном случае это index-directory. Второй параметр указывает «анализатор документов» или «анализатор документов», который будет использоваться, когда Lucene индексирует ваши данные. Здесь мы используем StandardAnalyzer
для этой цели. Более подробная информация об анализаторах Lucene приведена в ближайшее время. Третий параметр указывает Lucene создать новый индекс, если индекс еще не был создан в каталоге.
Документ — это единица процесса индексации и поиска.
Поля являются фактическими владельцами содержимого Lucene. В основном это хеш-таблица с именем и значением.
IndexWriter
создает и поддерживает индекс.
Аргумент create
конструктора определяет, будет ли создан новый индекс или открыт ли существующий индекс. Мы можем открыть индекс с помощью ‘create = true’, даже когда читатели используют индекс. Старые читатели продолжат поиск моментального снимка «момент времени», который они открыли, и не увидят вновь созданный индекс, пока они не откроются снова. Существуют также конструкторы без аргумента create, которые будут создавать новый индекс, если индекс по указанному пути еще не существует, и в противном случае открывать существующий индекс.
Изменения, сделанные во время вышеупомянутых вызовов методов, буферизируются в памяти, и очистка запускается, когда имеется достаточно буферизованных удалений или добавленных документов с момента последней очистки, в зависимости от того, что произойдет раньше. Флеш можно также вызвать принудительно. Когда происходит очистка, и ожидающие удаления, и добавленные документы сбрасываются в индекс. Сброс может также вызвать одно или несколько слияний сегментов.
Необязательный аргумент autoCommit
для конструкторов управляет видимостью изменений в экземплярах IndexReader
читающих тот же индекс. Когда это ложно, изменения не видны до close()
. Изменения будут по-прежнему отправляться в каталог как новые файлы, но они не фиксируются (новый файл сегментов_N, ссылающийся на новые файлы, не записывается) до close()
. Если что-то пойдет не так (например, сбой JVM) перед close()
, тогда индекс не отразит ни одно из внесенных изменений (он останется в своем начальном состоянии). Мы также можем вызвать abort()
, которая закрывает программу записи без внесения каких-либо изменений и удаляет все индексные файлы, которые были сброшены, но на которые теперь нет ссылок. Этот режим полезен для предотвращения обновления читателей в неподходящее время (например, после того, как мы сделали все удаления, но до того, как мы сделали добавления). Его также можно использовать для реализации простой семантики транзакций с одним автором («все или ничего»).
Когда autoCommit
имеет значение true, то каждый сброс также является коммитом. При работе в этом режиме следует иметь в виду, что считыватели не должны обновляться во время оптимизации или слияния сегментов, поскольку это может занять значительное дисковое пространство.
Независимо от autoCommit
, IndexReader
или IndexSearcher
будут видеть индекс только в «момент времени», в котором он был открыт. Любые изменения, внесенные в индекс после открытия считывателя, не видны до тех пор, пока считыватель не будет вновь открыт.
Если к индексу на некоторое время не будет добавлено больше документов, и требуется оптимальная производительность поиска, то перед закрытием индекса необходимо вызвать метод оптимизации.
Открытие IndexWriter
создает файл блокировки для используемого каталога. Попытка открыть другой IndexWriter
в том же каталоге приведет к LockObtainFailedException
. LockObtainFailedException
также генерируется, если IndexReader
в том же каталоге используется для удаления документов из индекса.
Основные поисковые классы
Основные классы поиска:
- IndexSearcher
- Срок
- запрос
- TermQuery
- TopDocs
Lucene использует экземпляры метко названного IndexReader
для чтения данных из индекса.
Lucene предоставляет класс IndexSearcher
который выполняет фактический поиск. Каждый поисковик индекса упаковывает читатель индекса, чтобы получить доступ к индексированным данным. Как только у нас будет поисковый индекс, мы можем предоставить ему запросы и перечислить результаты в порядке их оценки. В поисковике индекса действительно нечего настраивать, кроме его читателя.
IndexSearcher
полностью IndexSearcher
, то есть несколько потоков могут одновременно вызывать любой из его методов. Если приложение требует внешней синхронизации, нет необходимости синхронизировать экземпляр IndexSearcher
; вместо этого мы можем использовать наши собственные (не Lucene) объекты.
Вот синтаксис класса IndexReader
:
1
|
IndexSearcher is = new IndexSearcher(path); |
Запрос разбит на термины и операторы. Есть два типа терминов: отдельные термины и фразы.
Один термин — это одно слово, такое как «тест» или «привет».
Фраза — это группа слов, заключенная в двойные кавычки, такие как «Привет, пользователь».
Несколько терминов могут быть объединены вместе с логическими операторами, чтобы сформировать более сложный запрос.
Lucene поддерживает полевые данные, которые модули поиска Lucene API часто используют при многогранном поиске. По умолчанию Search Lucene API выполняет поиск в поле содержимого. Однако вы можете искать данные в определенном поле, введя имя поля, двоеточие «:» и искомый термин.
Например, если мы ищем узел под названием «Правильный путь», который содержит текст «go», мы можем ввести:
1
|
title: "The Right Way" AND contents:go |
или же
1
|
title: "The Right Way" AND go |
Так как содержимое является полем по умолчанию, индикатор поля не требуется.
Поле действительно только для термина, которому оно предшествует, поэтому запрос
1
|
title:Right Way |
найдет только «Право» в поле заголовка. Он попытается найти «путь» в поле по умолчанию (в данном случае это поле содержимого).
Все доступные типы полей перечислены ниже,
- UnStored
- Ключевое слово
- неиндексированный
- Текст
- двоичный
TopDocs
представляет собой набор документов, которые сортируются после поиска с использованием строки запроса. Самые подходящие документы перечислены вверху в TopDocs.
Для операции поиска необходим класс IndexSearcher
, который реализует основные методы поиска. Для каждого поиска необходим новый объект Query, который можно получить из экземпляра QueryParser
. Обратите внимание, что QueryParser
должен быть создан с использованием того же типа Analyzer, с которым был создан индекс, в нашем случае с использованием SimpleAnalyzer
. Версия также используется в качестве аргумента конструктора и является классом, который «используется определенными классами для соответствия совместимости версий между выпусками Lucene», согласно JavaDocs.
Когда поиск выполняется IndexSearcher
, в TopDocs
выполнения возвращается объект TopDocs
. Этот класс просто представляет результаты поиска и позволяет нам получать объекты ScoreDoc
. Используя ScoreDocs
мы находим Документы, которые соответствуют нашим критериям поиска, и из этих Документов мы получаем ScoreDocs
информацию. Давайте посмотрим все это в действии.
4. Простое приложение для поиска, использующее Apache Lucene
Прежде чем мы запустим наше первое поисковое приложение, мы должны загрузить последнюю версию Lucene.
Мы загрузили файлы jar Lucene версии 4.6.
Затем мы должны создать проект с именем LuceneWink и добавить файл jar в путь к классам проекта.
Прежде чем мы начнем выполнять поисковые запросы, нам нужно создать индекс, по которому будут выполняться запросы. Это будет сделано с помощью класса IndexWriter
, который создает и поддерживает индекс. IndexWriter получает документы в качестве входных данных, где документы являются единицей индексации и поиска. Каждый Document
на самом деле представляет собой набор полей, и каждое поле имеет имя и текстовое значение. Для создания IndexWriter
требуется анализатор. Этот класс является абстрактным, и конкретной реализацией, которую мы будем использовать, является SimpleAnalyzer
.
Мы попытаемся найти файлы, которые содержат строку, которую мы предоставим в качестве запроса в приложении ниже.
Итак, мы должны построить индекс файлов и выполнить поиск по ним, среди которых мы будем проводить наши поисковые операции.
Вот пример программы и комментарии приведены в строке:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
package com.wf.lucene; import java.io.File; import java.io.FileReader; import java.util.ArrayList; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopScoreDocCollector; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; public class LuceneOnFileSystemExample { static String DATA_FOLDER = "/home/piyas/Documents/Winkframe/sample_text_files/drugs/" ; // Where the files are. static String INDEX_FOLDER = "/home/piyas/Documents/Winkframe/sample_text_files/drugindex/" ; // Where the Index files are. private static StandardAnalyzer analyzer = new StandardAnalyzer(Version.LUCENE_46); private static IndexWriter writer; private static ArrayList<File> queue = new ArrayList<File>(); public static void indexFilesAndShowResults(String dataFilePath,String indexFilePath,String searchTerm) throws Exception { // Indexing part indexOnThisPath(indexFilePath); // Function for setting the Index Path indexFileOrDirectory(dataFilePath); // Indexing the files closeIndex(); //Function for closing the files // Search Part searchInIndexAndShowResult(indexFilePath, searchTerm); } public static void searchInIndexAndShowResult(String indexFilePath,String searchString) throws Exception{ IndexReader reader = DirectoryReader.open(FSDirectory.open( new File(indexFilePath))); // The api call to read the index IndexSearcher searcher = new IndexSearcher(reader); // The Index Searcher Component TopScoreDocCollector collector = TopScoreDocCollector.create( 5 , true ); Query q = new QueryParser(Version.LUCENE_46, "contents" , analyzer).parse(searchString); searcher.search(q, collector); ScoreDoc[] hits = collector.topDocs().scoreDocs; // display results System.out.println( "Found " + hits.length + " hits." ); for ( int i= 0 ;i<hits.length;++i) { int docId = hits[i].doc; Document d = searcher.doc(docId); System.out.println((i + 1 ) + ". " + d.get( "path" ) + " score=" + hits[i].score); // Found the document } } public static void closeIndex() throws Exception { writer.close(); // Close the Index } public static void indexOnThisPath(String indexDir) throws Exception { // the boolean true parameter means to create a new index everytime, // potentially overwriting any existing files there. FSDirectory dir = FSDirectory.open( new File(indexDir)); IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_46, analyzer); writer = new IndexWriter(dir, config); } /** * Indexes a file or directory * @param fileName the name of a text file or a folder we wish to add to the index * @throws java.io.IOException when exception */ public static void indexFileOrDirectory(String filePath) throws Exception { // Adding the files in lucene index //=================================================== //gets the list of files in a folder (if user has submitted //the name of a folder) //=================================================== addFiles( new File(filePath)); int originalNumDocs = writer.numDocs(); for (File f : queue) { FileReader fr = null ; try { Document doc = new Document(); //=================================================== // add contents of file //=================================================== fr = new FileReader(f); doc.add( new TextField( "contents" , fr)); doc.add( new StringField( "path" , f.getPath(), Field.Store.YES)); doc.add( new StringField( "filename" , f.getName(), Field.Store.YES)); writer.addDocument(doc); System.out.println( "Added: " + f); } catch (Exception e) { System.out.println( "Could not add: " + f); } finally { fr.close(); } } int newNumDocs = writer.numDocs(); System.out.println( "" ); System.out.println( "************************" ); System.out.println((newNumDocs - originalNumDocs) + " documents added." ); System.out.println( "************************" ); queue.clear(); } private static void addFiles(File file) { if (!file.exists()) { System.out.println(file + " does not exist." ); } if (file.isDirectory()) { for (File f : file.listFiles()) { addFiles(f); } } else { String filename = file.getName().toLowerCase(); //=================================================== // Only index text files //=================================================== if (filename.endsWith( ".htm" ) || filename.endsWith( ".html" ) || filename.endsWith( ".xml" ) || filename.endsWith( ".txt" )) { queue.add(file); } else { System.out.println( "Skipped " + filename); } } } /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { indexFilesAndShowResults(DATA_FOLDER,INDEX_FOLDER, "HIV" ); // Indexing files and Searching the word from files. } catch (Exception e) { e.printStackTrace(); } } } |
Наш образец приложения прилагается к этой статье.
Мы предоставляем каталог индекса, строку поискового запроса и максимальное количество обращений, а затем вызываем метод searchIndex
. В этом методе мы создаем IndexSearcher
, QueryParser
и объект Query. Обратите внимание, что QueryParser
использует имя поля, которое мы использовали для создания документов с помощью IndexWriter
(«содержимое»), и снова, что используется тот же тип анализатора ( SimpleAnalyzer
). Мы выполняем поиск и для каждого Документа, в котором найдено совпадение, извлекаем значение поля, содержащего имя файла («имя файла»), и печатаем его.
Здесь мы сделали простое приложение для поиска с использованием Apache Lucene. В следующих статьях мы будем использовать более сложные запросы и другие расширенные параметры индексации и поиска Lucene в деталях.