Lucene
Lucene是apache下的一个开源的全文检索引擎工具包。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现全文检索的功能。
Lucene和搜索引擎是不同的,Lucene是一套用java或其它语言写的全文检索的工具包。它为应用程序提供了很多个api接口去调用,可以简单理解为是一套实现全文检索的类库。搜索引擎是一个全文检索系统,它是一个单独运行的软件系统。
实现全文检索
A. 索引流程
对文档索引的过程,就是将用户要搜索的文档内容进行索引,然后把索引存储在索引库(index)中。
-
为什么要采集数据
全文检索搜索的内容的格式是多种多样的,比如:视频、mp3、图片、文档等等。对于这种格式不同的数据,需要先将他们采集到本地,然后统一封装到lucene的文档对象中,也就是说需要将存储的内容进行统一才能对它进行查询。
-
采集数据的方式
- 对于互联网中的数据,使用爬虫工具(http工具)将网页爬取到本地
- 对于数据库中的数据,使用jdbc程序进行数据采集
- 对于文件系统的数据,使用io流采集
-
索引文件的逻辑结构
文档域:
-
文档域存储的信息就是采集到的信息,通过Document对象来存储,具体说是通过Document对象中field域来存储数据。
比如:数据库中一条记录会存储一个一个Document对象,数据库中一列会存储成Document中一个field域。
-
文档域中,Document对象之间是没有关系的。而且每个Document中的field域也不一定一样。
Field Data Type Analyzed Indexed Stored Note StringField (FieldName, FieldValue, Store.YES) String N Y Y / N 构建字符串Field,但是不会进行分词,将整个串储存在索引中。是否储存在文档中由Store.YES(NO)决定。 LongField (FieldName, FieldValue, Store.YES) Long Y Y Y / N 构建Long数字型Field,进行分词和索引。 是否储存在文档中由Store.YES(NO)决定。 StoredField (FieldName, FieldValue) Multiple Types N N Y 构建不同类型Field,不分析、不索引,储存在文档中。 TextField (FieldName, FieldValue, Store.NO) / TextField (FieldName, reader) String / Steam Y Y Y / N 如果是Reader,lucene预判内容比较多,会采用Unstored的策略。 索引域:
- 索引域主要是为了搜索使用的。索引域内容是经过lucene分词之后存储的。
倒排索引表:
- 传统方法是先找到文件,如何在文件中找内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大就搜索慢。
- 倒排索引结构是根据内容(词语)找文档,倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它是在索引中匹配搜索关键字,由于索引内容量有限并且采用固定优化算法搜索速度很快,找到了索引中的词汇,词汇与文档关联,从而最终找到了文档。
-
-
索引
4.1 采集数据
Book:
public class Book { private Integer id; private String name; private Float price; private String pic; private String description; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Float getPrice() { return price; } public void setPrice(Float price) { this.price = price; } public String getPic() { return pic; } public void setPic(String pic) { this.pic = pic; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
BookDao:
public interface BookDao { public List<Book> queryBooks(); }
BookDaoImpl:
public class BookDaoImpl implements BookDao { @Override public List<Book> queryBooks() { // 数据库链接 Connection connection = null; // 预编译statement PreparedStatement preparedStatement = null; // 结果集 ResultSet resultSet = null; // 图书列表 List<Book> list = new ArrayList<Book>(); try { // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 连接数据库 connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/solr", "root", "root"); // SQL语句 String sql = "SELECT * FROM book"; // 创建preparedStatement preparedStatement = connection.prepareStatement(sql); // 获取结果集 resultSet = preparedStatement.executeQuery(); // 结果集解析 while (resultSet.next()) { Book book = new Book(); book.setId(resultSet.getInt("id")); book.setName(resultSet.getString("name")); book.setPrice(resultSet.getFloat("price")); book.setPic(resultSet.getString("pic")); book.setDescription(resultSet.getString("description")); list.add(book); } } catch (Exception e) { e.printStackTrace(); } return list; } }
4.2 创建索引
创建索引流程:
IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。
Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。
代码:
@Test public void createIndex() throws Exception { // 采集数据 BookDao dao = new BookDaoImpl(); List<Book> list = dao.queryBooks(); // 将采集到的数据封装到Document对象中 List<Document> docList = new ArrayList<>(); Document document; for (Book book : list) { document = new Document(); // store:如果是yes,则说明存储到文档域中 // 图书ID // 不分词、索引、存储 StringField Field id = new StringField("id", book.getId().toString(), Store.YES); // 图书名称 // 分词、索引、存储 TextField Field name = new TextField("name", book.getName(), Store.YES); // 图书价格 // 分词、索引、存储 但是是数字类型,所以使用FloatField Field price = new FloatField("price", book.getPrice(), Store.YES); // 图书图片地址 // 不分词、不索引、存储 StoredField Field pic = new StoredField("pic", book.getPic()); // 图书描述 // 分词、索引、不存储 TextField Field description = new TextField("description", book.getDescription(), Store.NO); // 设置boost值 if (book.getId() == 4) description.setBoost(100f); // 将field域设置到Document对象中 document.add(id); document.add(name); document.add(price); document.add(pic); document.add(description); docList.add(document); } // 创建分词器,标准分词器 // Analyzer analyzer = new StandardAnalyzer(); // 使用ikanalyzer Analyzer analyzer = new IKAnalyzer(); // 创建IndexWriter IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // 指定索引库的地址 File indexFile = new File("E:\\11-index\\hcx\\"); Directory directory = FSDirectory.open(indexFile); IndexWriter writer = new IndexWriter(directory, cfg); // 通过IndexWriter对象将Document写入到索引库中 for (Document doc : docList) { writer.addDocument(doc); } // 关闭writer writer.close(); }
4.3 分词
在对Docuemnt中的内容索引之前需要使用分词器进行分词 ,主要过程就是分词、过虑两步。
- 分词:将采集到的文档内容切分成一个一个的词,具体应该说是将Document中Field的value值切分成一个一个的词。
- 过滤:将分好的词进行过滤,包括去除标点符号、去除停用词(的、是、a、an、the等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。
什么是停用词?
- 停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。
Lucene作为了一个工具包提供不同国家的分词器,如下图:
注意由于语言不同分析器的切分规则也不同,本例子使用StandardAnalyzer,它可以对用英文进行分词。
如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:
@Override protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) { final StandardTokenizer src = new StandardTokenizer(getVersion(), reader); src.setMaxTokenLength(maxTokenLength); TokenStream tok = new StandardFilter(getVersion(), src); tok = new LowerCaseFilter(getVersion(), tok); tok = new StopFilter(getVersion(), tok, stopwords); return new TokenStreamComponents(src, tok) { @Override protected void setReader(final Reader reader) throws IOException { src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength); super.setReader(reader); } }; }
- Tokenizer是分词器,负责将reader转换为语汇单元即进行分词,Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。
- TokenFilter是分词过滤器,负责对语汇单元进行过滤,tokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。
如下图是语汇单元的生成过程:
从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。
比如下边的文档经过分析器分析如下:
原文档内容: Lucene is a Java full-text search engine. 分析后得到的语汇单元: lucene、java、full、text、search、engine
同一个域中相同的语汇单元(Token)对应同一个Term(词),它记录了语汇单元的内容及所在域的域名等,还包括该token出现的频率及位置。
- 不同的域中拆分出来的相同的单词对应不同的term。
- 相同的域中拆分出来的相同的单词对应相同的term。
例如:图书信息里面,图书名称中的java和图书描述中的java对应不同的term
4.4 使用luke工具查看索引
Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过界面来进行索引文件的查询、修改。
打开Luke方法:
- 命令运行:cmd运行:java -jar lukeall-4.10.3.jar
- 手动执行:双击lukeall-4.10.3.jar
B. 搜索流程
搜索过程:
- 用户定义查询语句,用户确定查询什么内容(输入什么关键字)指定查询语法,相当于sql语句。
- IndexSearcher索引搜索对象,定义了很多搜索方法,程序员调用此方法搜索。
- IndexReader索引读取对象,它对应的索引维护对象IndexWriter,IndexSearcher通过IndexReader读取索引目录中的索引文件
- Directory索引流对象,IndexReader需要Directory读取索引库,使用FSDirectory文件系统流对象
- IndexSearcher搜索完成,返回一个TopDocs(匹配度高的前边的一些记录)
1.输入查询语句:
同数据库的sql一样,lucene全文检索也有固定的语法: 最基本的有比如:AND, OR, NOT 等
举个例子,用户想找一个description中包括java关键字和lucene关键字的文档。 它对应的查询语句:description:java AND lucene 如下是使用luke搜索的例子:
2.搜索分词
和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。
比如:输入搜索关键字“java学习”,分词后为java和学习两个词,与java和学习有关的内容都搜索出来了
代码:
@Test
public void indexSearch() throws Exception {
// 创建query对象
// 使用QueryParser搜索时,需要指定分词器,搜索时的分词器要和索引时的分词器一致
// 第一个参数:默认搜索的域的名称
QueryParser parser = new QueryParser("description",
new StandardAnalyzer());
// 通过queryparser来创建query对象
// 参数:输入的lucene的查询语句(关键字一定要大写)
Query query = parser.parse("description:java AND lucene");
// 创建IndexSearcher
// 指定索引库的地址
File indexFile = new File("E:\\11-index\\hcx\\");
Directory directory = FSDirectory.open(indexFile);
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
// 通过searcher来搜索索引库
// 第二个参数:指定需要显示的顶部记录的N条
TopDocs topDocs = searcher.search(query, 10);
// 根据查询条件匹配出的记录总数
int count = topDocs.totalHits;
System.out.println("匹配出的记录总数:" + count);
// 根据查询条件匹配出的记录
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取文档的ID
int docId = scoreDoc.doc;
// 通过ID获取文档
Document doc = searcher.doc(docId);
System.out.println("商品ID:" + doc.get("id"));
System.out.println("商品名称:" + doc.get("name"));
System.out.println("商品价格:" + doc.get("price"));
System.out.println("商品图片地址:" + doc.get("pic"));
System.out.println("==========================");
// System.out.println("商品描述:" + doc.get("description"));
}
// 关闭资源
reader.close();
}