一、什么是全文检索
1、数据的分类
1)结构化数据
格式固定、长度固定、数据类型固定。
例如数据库中的数据
2)非结构化数据
word文档、pdf文档、邮件、html、txt
格式不固定、长度不固定、数据类型不固定。
2、数据的查询
1)结构化数据的查询
SQL语句,查询结构化数据的方法。简单、速度快。
2)非结构化数据的查询
从文本文件中找出包含spring单词的文件。
1、目测
2、使用程序吧文档读取到内存中,然后匹配字符串。顺序扫描。
3、把非结构化数据变成结构化数据
先跟根据空格进行字符串拆分,得到一个单词列表,基于单词列表创建一个索引。
然后查询索引,根据单词和文档的对应关系找到文档列表。这个过程叫做全文检索。
索引:一个为了提高查询速度,创建某种数据结构的集合。
3、全文检索
先创建索引然后查询索引的过程叫做全文检索。
索引一次创建可以多次使用。表现为每次查询速度很快。
二、全文检索的应用场景
1、搜索引擎
百度、360搜索、谷歌、搜狗
2、站内搜索
论坛搜索、微博、文章搜索
3、电商搜索
淘宝搜索、京东搜索
4、只要是有搜索的地方就可以使用全文检索技术。
三、什么是Lucene
Lucene是一个基于Java开发全文检索工具包。
就Java来说,能实现搜索的只有Lucene,即使后出现的ElasticSearch,也是基于Lucene
所以掌握lucene对以后还是有帮助的。
四、Lucene实现全文检索的流程
1)获得文档
原始文档:要基于那些数据来进行搜索,那么这些数据就是原始文档。
搜索引擎:使用爬虫获得原始文档
站内搜索:数据库中的数据。
案例:直接使用io流读取磁盘上的文件。
使用IO流中的FileReader的话,获取内容就比较麻烦,
1.创建出FileReader都西昂
2.创建一个char【】数组
3.read读取到-1截止
4.使用String ste=new String 获取内容。
而我们用Common-IO的方法就简单多了
2)构建文档对象
对应每个原始文档创建一个Document对象
每个document对象中包含多个域(field)
域中保存就是原始文档数据。
域的名称
域的值
每个文档都有一个唯一的编号,就是文档id
3)分析文档
就是分词的过程
1、根据空格进行字符串拆分,得到一个单词列表
2、把单词统一转换成小写。
3、去除标点符号
4、去除停用词
停用词:无意义的词
每个关键词都封装成一个Term对象中。
Term中包含两部分内容:
关键词所在的域
关键词本身
不同的域中拆分出来的相同的关键词是不同的Term。
4)创建索引
基于关键词列表创建一个索引。保存到索引库中。
索引库中:
索引
document对象
关键词和文档的对应关系
通过词语找文档,这种索引的结构叫倒排索引结构。
@Test
public void createIndex() throws Exception {
// 创建索引步骤
// 1、创建一个Director对象,指定索引库保存的位置。
//把索引库保存到内存中,特点:速度快,但是内存关闭,索引库就没了,所以不用这种方法
// Directory directory=new RAMDirectory() {}
//把索引库保存到磁盘
Directory directory = FSDirectory.open(new File("E:\\temp\\lucense").toPath());
// 2、基于Directory对象创建一个IndexWriter索引对象
// IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
//2.1,使用中文分析器
IndexWriterConfig config=new IndexWriterConfig(new IKAnalyzer());
IndexWriter indexWriter = new IndexWriter(directory, config);
// 3、读取磁盘上的文件,对应每个文件创建一个文档对象。
File dir = new File("F:\\BaiduNetdiskDownload\\searchsource");
//获取该路径下文件或者文件夹组成的File数组
File[] list = dir.listFiles();
for (File file : list) {
//得到最后一层的名字
String filename = file.getName();
//文件的路径
String filePath = file.getPath();
//获取文件的内容,本来需要用IO流的,但是有了Common-io的jar包,就可以用
String fileContent = FileUtils.readFileToString(file, "utf-8");
//文件大小
long fileSize = FileUtils.sizeOf(file);
//创建Field域,每一个属性都要创建一个域
//参数1:域的名称。参数2:域的内容,参数三:是否存储(把域的内容保存到磁盘)
Field fieldName=new TextField("name",filename,Field.Store.YES);
// Field fieldPath=new TextField("path",filePath,Field.Store.YES);
//path是不需要索引不需要分词的,所以用StoreField,只存储
StoredField fieldPath=new StoredField("path",filePath);
Field fieldContent=new TextField("content",fileContent,Field.Store.YES);
// Field fieldSize=new TextField("size",fileSize+"",Field.Store.YES);
//size只是一个长整型的值,不需要分词,只索引,不存储,不过不能拿出来,只能进行运算,
Field fieldSizeValue=new LongPoint("size",fileSize);
//如果想取到size的值,还需要用store储存,不分词,不索引
Field fieldSizeStore=new StoredField("size",fileSize);
//创建文档对象
//4、向文档对象中添加域
Document document=new Document();
document.add(fieldName);
document.add(fieldPath);
document.add(fieldContent);
// document.add(fieldSize);
document.add(fieldSizeValue);
document.add(fieldSizeStore);
// 5、把文档对象写入索引库,索引库中就有了文档对象了。
indexWriter.addDocument(document);
}
// 6、关闭indexwriter对象
indexWriter.close();
}
五、索引的分类
1、可以看到我们上面创建索引的时候用到的是Field类,而他有许多的子类,我们可以根据下表来分析,我们创建的索引需要用哪个字类创建。Y代Yes,N代表No。
Field类 |
数据类型 |
Analyzed 是否分析 |
Indexed 是否索引 |
Stored 是否存储 |
说明 |
StringField(FieldName, FieldValue,Store.YES)) |
字符串 |
N |
Y |
Y或N |
这个Field用来构建一个字符串Field,但是不会进行分析,会将整个串存储在索引中,比如(订单号,姓名等) 是否存储在文档中用Store.YES或Store.NO决定 |
LongPoint(String name, long... point) |
Long型 |
Y |
Y |
N |
可以使用LongPoint、IntPoint等类型存储数值类型的数据。让数值类型可以进行索引。但是不能存储数据,如果想存储数据还需要使用StoredField。 |
StoredField(FieldName, FieldValue) |
重载方法,支持多种类型 |
N |
N |
Y |
这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中 |
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader) |
字符串 或 流 |
Y |
Y |
Y或N |
如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. |
六、索引的查询
1、我们可以用luke这个软件查询
2、我们也可以写代码进行查询,会用到indexSearcher,indexReader两个类
1)第一步依旧是创建directory对象,获取索引库
2)把indexSearcher,indexReader对象创建出来
3)使用Query查询,里面有两种字类,一个是TermQuery,还有一个是QueryParser,前者是按照分词库中的词进行查询。后者是会帮我们把一段话进行分词,然后去分词库中查找关键字查询。
4)调用indexSearcher.search方法,把query传入,第二个参数是一次查询几个目标。
5)得到了topDocs,再利用其中的scoreDocs获取到文档列表
6)scoreDocs.doc获取文档的id
7)再次调用indexSearcher.doc(id)获取到document文件。
8)最后取出域即可
@Test
public void searchIndex() throws Exception{
// 查询索引库
// 步骤:
// 1、创建一个Director对象,指定索引库的位置
Directory directory=FSDirectory.open(new File("E:\\temp\\lucense").toPath());
// 2、创建一个IndexReader对象,放入索引库
IndexReader reader= DirectoryReader.open(directory);
// 3、创建一个IndexSearcher对象,构造方法中的参数indexReader对象。
IndexSearcher indexSearcher=new IndexSearcher(reader);
// 4、创建一个Query对象,TermQuery,term就是关键字,fld参数就是要查询的域,text,就是域中我们想要的内容
Query query=new TermQuery(new Term("name","全文"));
// 5、执行查询,得到一个TopDocs对象
//参数一就是查询对象
//n就是本次查询返回的最大记录数
TopDocs topDocs = indexSearcher.search(query, 10);
// 6、取查询结果的总记录数
System.out.println("查询总记录数为"+topDocs.totalHits);
// 7、取文档列表
ScoreDoc[] scoreDocs=topDocs.scoreDocs;
// 8、打印文档中的内容
for (ScoreDoc scoreDoc : scoreDocs) {
//取文档id
int docId = scoreDoc.doc;
//根据id取文档对象
Document document = indexSearcher.doc(docId);
//取出文档中的域
System.out.println(document.get("name"));
System.out.println(document.get("path"));
System.out.println(document.get("size"));
System.out.println(document.get("content"));
System.out.println("--------------");
}
// 9、关闭IndexReader对象
reader.close();
}
七、了解下分析器
这里我们用的是中文分析器,分词器的参考依据是我们需要自己写一个文档或者从网上下载我们的分词库。Ik解析器的配置,需要把扩展词典和停用词典配置上。
@Test
public void testTokenStream() throws Exception{
// 查看分析器的分析效果
// 使用Analyzer对象的tokenStream方法返回一个TokenStream对象。词对象中包含了最终分词结果。
// 实现步骤:
// 1)创建一个Analyzer对象,StandardAnalyzer对象
// Analyzer analyzer=new StandardAnalyzer();分析英文的
Analyzer analyzer=new IKAnalyzer();//分析中文的
// 2)使用分析器对象的tokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer.tokenStream("", "我可能今晚不回去了,去女朋友家里睡");
// 3)向TokenStream对象中设置一个引用,相当于数一个指针
CharTermAttribute charTermAttribute=tokenStream.addAttribute(CharTermAttribute.class);
// 4)调用TokenStream对象的reset方法。如果不调用抛异常
tokenStream.reset();
// 5)使用while循环遍历TokenStream对象
while(tokenStream.incrementToken()){
System.out.println(charTermAttribute.toString());
}
// 6)关闭TokenStream对象
tokenStream.close();
}
分词结果:
我
可能
今晚
不回
回去
去了
去
女朋友
朋友
家里
睡
八、索引库的添加操作,就是和第一个代码一样,添加域和文件,这里我简写了,没有获取新的文件,用字符串代替索引的内容。
@Test
public void addIndex() throws Exception {
// Directory directory= FSDirectory.open(new File("E:\\temp\\lucense").toPath());
// //基于Directory创建一个索引对象
// IndexWriterConfig config=new IndexWriterConfig(new IKAnalyzer());
// IndexWriter indexWriter=new IndexWriter(directory,config);
IndexWriter indexWriter=getIndexWriter();
//创建document对象
Document document=new Document();
document.add(new TextField("name","这是新的索引对象", Field.Store.YES));
document.add(new TextField("content","内容就是没什么内容", Field.Store.YES));
document.add(new StoredField("size","这个文件大小为10kb"));
indexWriter.addDocument(document);
indexWriter.close();
}
九、索引库的删除
1)删除全部索引
2)删除指定索引
可以看到删除指定索引的参数可以是Term,也可以是Query
@Test
public void delAllIndex() throws Exception{
IndexWriter indexWriter=getIndexWriter();
indexWriter.deleteAll();
indexWriter.close();
}
@Test
public void delIndexBySelect() throws Exception{
IndexWriter indexWriter=getIndexWriter();
Query query=new TermQuery(new Term("name","全文"));
indexWriter.deleteDocuments(query);
indexWriter.close();
}
十、更新索引
1)其原理就是先删除指定索引,然后再添加索引,依旧用到indexWriter
2)indexWriter.updateDocument两个参数,第一个是先查询,然后再删除(如果查到多个,就删除多个),第二个参数就是我们要添加的索引
@Test
public void updateDocument() throws Exception{
//原理是先删除查询到的文档,然后添加文档
IndexWriter indexWriter=getIndexWriter();
Document document=new Document();
document.add(new TextField("name","这是更新的文档", Field.Store.YES));
document.add(new TextField("name1","这是更新的文档1", Field.Store.YES));
document.add(new TextField("name2","这是更新的文档2", Field.Store.YES));
//先把name域中的包含全文的document全部删除,然后加入这个document
indexWriter.updateDocument(new Term("name","全文"),document);
indexWriter.close();
}
十一、分词查询(可以近似于百度一样的把我们要查询的语句进行分词,然后进索引库查询)
1)这里Directory和indexSearcher,indexReader都在外部声明了。
2)用的就是我们上面讲过的QueryParser。
@Test
public void testQueryParser() throws Exception{
//创建一个QueryParser对象,两个参数
//参数1:默认搜索域,参数2:分析器对象
QueryParser queryParser=new QueryParser("name",new IKAnalyzer());
//使用QueryParser对象创建一个Query对象,这句话首先会使用Ik分析器分词,然后在查询。
Query query = queryParser.parse("lucene是一个java开发的全文检索工具包");
//执行查询
TopDocs topDocs = indexSearcher.search(query, 20);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
System.out.println("总记录数为:"+topDocs.totalHits);
for (ScoreDoc scoreDoc : scoreDocs) {
int docId = scoreDoc.doc;
Document document=indexSearcher.doc(docId);
System.out.println(document.get("name"));
System.out.println(document.get("path"));
System.out.println(document.get("size"));
// System.out.println(document.get("content"));
System.out.println("--------------------");
}
indexReader.close();
}
【注意】lucene需要在jdk1.8版本以上才能运行
以上代码需要导入的jar包