搜索工具,他的原理大概如下:
爬虫工具会去互联网中的各个网站爬取信息,比如网站的url,名字,内容。这些数据都是非结构化的数据,而全文检索技术就是负责把这些非结构化数据整理成结构化的数据,对这些结构化的数据做一系列的处理,然后生成一个索引表,前端传递过来的查询条件就会去这个索引表中寻找对应的结构化文件。
lucene是一种全文检索的工具包,使用lucene可以实现全文检索。
document是存储那些整理后数据的对象。比如一个网页
web:
标题:腾讯视频
内容:创造202 刘念c位出道
作者:刘翔华
经过整理后,在document中:
document:
id:1(假如是1)
title:腾讯视频
content:创造202 刘念c位出道
writer:刘翔华
分词器的作用,就是把document中的词给筛选出来,形成一个一个的词项,用这个词项来完成索引表的建立。比如:创造202 刘念c位出道,分词后可以形成,创造202(1),刘念(1),c位出道 (1)。
分词器接口 :Analyzer,是所有分词器的接口。
分词种类:
StandardAnalyzer–>标准分词器
SimpleAnalyzer—>简单分词器
WhitespaceAnalyzer—>空格分词器
SmartChineseAnalyzer—>智能中文分词器
IKAnalyzer—>中文分词器
还支持停用词典:在停用词典中的词语,不会计算分词
public class AnalyzerTest {
//完成方法,接收文本字符串,通过使用分词器的api将词项的文本属性
//打印出来
public void printTerm(Analyzer a, String msg) throws Exception {
//使用a实现对象分词器,解析msg分词计算;
//String原数据转化成流对象
StringReader reader=new StringReader(msg);
//调用a这个分词的api将read流计算成词项
TokenStream token = a.tokenStream("test", reader);//分词不能独立存在,依托document数据
token.reset();
//拿到当前指针位置的词项的文本属性
OffsetAttribute offAttr = token.getAttribute(OffsetAttribute.class);
CharTermAttribute charAttr = token.getAttribute(CharTermAttribute.class);
while(token.incrementToken()){
//System.out.println("偏移量起始位置:"+offAttr.startOffset());
//System.out.println("偏移量结束位置:"+offAttr.endOffset());
System.out.println(charAttr.toString());
}
}
@Test
public void run() throws Exception {
//构造多个不同实现类的分词器
Analyzer a1=new StandardAnalyzer();
Analyzer a2=new SimpleAnalyzer();
Analyzer a3=new WhitespaceAnalyzer();
Analyzer a4=new SmartChineseAnalyzer();
Analyzer a5=new IKAnalyzer6x();
//计算分词的文本字符串
String msg="刘念刘翔华";
/*System.out.println("******************标准********************");
printTerm(a1,msg);
System.out.println("******************简单********************");
printTerm(a2,msg);
System.out.println("******************空格********************");
printTerm(a3,msg);*/
System.out.println("******************智能********************");
printTerm(a4,msg);
System.out.println("******************IK********************");
printTerm(a5,msg);
}
}
索引文件是基于document的分词来创建的。
document:
id:1(假如是1)
title:腾讯视频
content:创造202 刘念c位出道
writer:刘翔华
document:
id:2(假如是2)
title:优酷视频
content:一拳超人新作
writer:PPT
分词后:
doc1:腾讯(1),视频(1),创造202(1),刘念(1),c位出道(1),刘翔华(1)
doc2:优酷(2),视频(2),一拳超人(2),新作(2),PPT(2)
索引表:
根据分词结果,创建如下的索引表。
比如我要找有一拳超人,还要有PPT的资源,就根据一拳超人,找到doc2,再找到PPT,还是doc2.
创建索引表的逻辑:
选择一个文件夹,作为保存索引数据的目标
创建一个输出流对象Writer
确定分词规则
读取数据源,封装document对象数据
把document输出
把索引文件输出
代码:
需要注意的是这一句IndexWriterConfig config=new IndexWriterConfig(new IKAnalyzer6x());确定了分词规则。
public class CreateIndex {
//实现lucene的创建索引文件
@Test
public void createIndex() throws Exception {
//指定到一个文件目录 d:/index01
Path path = Paths.get("d:/index01");
//路径交给lucene管理
FSDirectory dir = FSDirectory.open(path);
//构造一个输出流对象writer
//配置输出流的配置对象config
IndexWriterConfig config=new IndexWriterConfig(new IKAnalyzer6x());
//创建索引文件的模式
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);//每次都会把旧数据
//覆盖掉
IndexWriter writer=new IndexWriter(dir,config);
//手动封装数据源数据 到document对象
Document doc1=new Document();
Document doc2=new Document();
//拼接数据到document 1 2当中 title content publisher click
//先来封装document1
/*
web1:
> 标题:腾讯视频
> 内容:创造202 刘念c位出道
> 作者:刘翔华
> 点击数量:58
*/
//封装title 文本字符串
doc1.add(new TextField("title","腾讯视频", Field.Store.YES));
//参数意义 name:域属性名称 value: 源数据 store:表示是否存储在索引中.
doc1.add(new TextField("content",
"创造202 刘念c位出道 ",
Field.Store.YES));
doc1.add(new TextField("writer","刘翔华", Field.Store.YES));
//数字类型数据 IntPoint LongPoint DoublePoint FloatPoint
doc1.add(new IntPoint("click",58));
doc1.add(new StringField("click","58次", Field.Store.YES));
/*
web2:
> document:
> id:2(假如是2)
> title:优酷视频
> content:一拳超人新作
> writer:PPT
>click: 66
*/
doc2.add(new TextField("title","优酷视频 ", Field.Store.YES));
doc2.add(new TextField("content",
"一拳超人新作",
Field.Store.YES));
doc2.add(new TextField("writer","PPT", Field.Store.YES));
doc2.add(new IntPoint("click",66));
doc2.add(new StringField("click","66次", Field.Store.YES));
//问题:
//域属性有多少个类型?
//StringField文本,TextField文本区别
//为什么doc可以存在索引也可以不存在索引中Store.YES/NO
//将document通过writer输出
writer.addDocument(doc1);
writer.addDocument(doc2);
//生成索引文件
writer.commit();
}
封装document时的细节:
StringField文本,TextField文本区别 :TextField类型的域属性value值会经过分词计算,StringField类型的域属性value值不会经过分词计算 。
数字类型:既不做分词计算,也不做存储,数字特性的数据在索引中单独处理,用来实现范围搜索(价钱范围)如果某个字段,既需要数字特性进行范围搜索,搜索到document之后又要使用这个数字数据–通过一个同名的StringField使得该域属性具备2个特点。
IntPoint
LongPoint
DoublPoint
FloatPoint
Store.YES/NO的作用: Store.YES索引中的文件document会保存该属性 ,Store.NO索引中没有这个域值保存在document 。
不存储的数据,不代表不计算分词 : 比如doc.add(new TextField(“test”,“我们都有一个家”,Store.NO)) ,因为使用了Store.NO,所以不会存储到索引表中,但是使用的是TextField,所以还是会做分词计算(索引表中有分词,但是查不到对应的document)。
在进行搜索时,lucene总会把搜索条件封装成一个query查询对象.通过对查询对象数据的计算最终拿到返回document数据.查询对象又可以根据查询条件不同,需求不同使用不同的实现类封装.
**深查询:**比如要查询索引表第二页的数据,一页有5条,那么就读取10条信息,然后返回后五条。先读取再计算,效率低。
浅查询: 先计算需要的页数和数据量,再去读取数据返回,先计算再读取,效率高。
词项查询:
就是你输入的查询词是什么,就用这个词去索引里查询,返回查询到的document集合返回。
//词项查询
@Test
public void termQuery() throws Exception {
//指向d:/index01
Path path = Paths.get("d:/index01");
FSDirectory dir = FSDirectory.open(path);
//搜索对象执行搜索时,要对索引进行计算,要读取数据,基于输入流reader创建
IndexReader reader= DirectoryReader.open(dir);
IndexSearcher searcher=new IndexSearcher(reader);
//创建查询条件
//创建一个TermQuery词项查询条件 "title":"腾讯视频"
Term term=new Term("title","腾讯视频");
Query query=new TermQuery(term);
//浅查询逻辑,获取数据信息docId读取数据.
//page=2 rows=5
TopDocs topDocs = searcher.search(query,2*5);//先从索引倒排索引表计算,获取前10条结果
System.out.println("查询总条数:"+topDocs.totalHits);
ScoreDoc[] scoreDocs=topDocs.scoreDocs;
//count值,当count=0 1 2 3 4不读数据 5 6 7 8 9
int count=0;
//从数组里,解析前10条docId值,通过id读取所有document数据
for (ScoreDoc scoreDoc:scoreDocs){
count++;
if(count>4){
//元素对象中就包含了一个documentId
int docId = scoreDoc.doc;
Document doc = searcher.doc(docId);
System.out.println("title:"+doc.get("title"));
System.out.println("content:"+doc.get("content"));
System.out.println("publisher"+doc.get("publisher"));
System.out.println("click"+doc.get("click"));
}
continue;
}
}
多域查询:
document中是由多个域属性构成,每个域属性中都存有自己的值,当你的查询条件是去一个document的多个域中查询的时候,就可以使用多域查询。
//多域查询
@Test
public void MultiFiedlQuery() throws Exception {
Path path = Paths.get("d:/index01");
FSDirectory dir = FSDirectory.open(path);
IndexReader reader= DirectoryReader.open(dir);
IndexSearcher searcher=new IndexSearcher(reader);
//创建查询条件
//多域查询
String[] fields={"title","content"};
//创建多域查询条件
MultiFieldQueryParser parser=new MultiFieldQueryParser(fields,new IKAnalyzer6x());
Query query = parser.parse("腾讯视频优酷视频");
//底层实现逻辑
//ik分词器对"腾讯视频优酷视频"-->"腾讯""优酷""视频"
//和string[]中的域名做 排列组合 title:腾讯,title:优酷,title:视频
//content:腾讯,content:优酷,content:视频 将每个结果单独搜索计算得到一个document结果集
//最终将所有对应结果集返回
TopDocs topDocs = searcher.search(query,10);//先从索引倒排索引表计算,获取前10条结果
System.out.println("查询总条数:"+topDocs.totalHits);
ScoreDoc[] scoreDocs=topDocs.scoreDocs;
//从数组里,解析前10条docId值,通过id读取所有document数据
for (ScoreDoc scoreDoc:scoreDocs){
//元素对象中就包含了一个documentId
int docId = scoreDoc.doc;
Document doc = searcher.doc(docId);
System.out.println("title:"+doc.get("title"));
System.out.println("content:"+doc.get("content"));
System.out.println("publisher"+doc.get("publisher"));
System.out.println("click"+doc.get("click"));
}
}
布尔查询:
把每个query当作一个集合,使用这些集合,就可以查询出他们的交集,并集等关系。
就像下面的这两句:
Query query1=new TermQuery(new Term(“title”,“腾讯视频”));
Query query2=new TermQuery(new Term(“content”,“优酷视频”));
BooleanClause bc1=new BooleanClause(query1, BooleanClause.Occur.MUST);
BooleanClause bc2=new BooleanClause(query2, BooleanClause.Occur.MUST_NOT);
Query query=new BooleanQuery.Builder().add(bc1).add(bc2).build();
语句意思就是,查询title为腾讯视频,content不为优酷视频的document。
@Test
public void booleanQuery() throws Exception {
Path path = Paths.get("d:/index01");
FSDirectory dir = FSDirectory.open(path);
IndexReader reader= DirectoryReader.open(dir);
IndexSearcher searcher=new IndexSearcher(reader);
//创建查询条件
//boolean条件
//子条件
Query query1=new TermQuery(new Term("title","腾讯视频"));
Query query2=new TermQuery(new Term("content","优酷视频"));
//封装子条件
BooleanClause bc1=new BooleanClause(query1, BooleanClause.Occur.MUST);
BooleanClause bc2=new BooleanClause(query2, BooleanClause.Occur.MUST_NOT);
//MUST 布尔查询结果必须是子条件的子集
//MUST_NOT 布尔查询结果必须不是子条件的子集
Query query=new BooleanQuery.Builder().add(bc1).add(bc2).build();
TopDocs topDocs = searcher.search(query,10);//先从索引倒排索引表计算,获取前10条结果
System.out.println("查询总条数:"+topDocs.totalHits);
ScoreDoc[] scoreDocs=topDocs.scoreDocs;
//从数组里,解析前10条docId值,通过id读取所有document数据
for (ScoreDoc scoreDoc:scoreDocs){
//元素对象中就包含了一个documentId
int docId = scoreDoc.doc;
Document doc = searcher.doc(docId);
System.out.println("title:"+doc.get("title"));
System.out.println("content:"+doc.get("content"));
System.out.println("publisher"+doc.get("publisher"));
System.out.println("click"+doc.get("click"));
}
}
范围查询:
针对IntPoint ,LongPoint ,DoublPoint ,FloatPoint 这些类型,可以进行范围查询。
public void rangeQuery() throws Exception {
Path path = Paths.get("d:/index01");
FSDirectory dir = FSDirectory.open(path);
IndexReader reader= DirectoryReader.open(dir);
IndexSearcher searcher=new IndexSearcher(reader);
//创建查询条件
//click属于IntPoint
Query query=IntPoint.newRangeQuery("click",60,100);//点击量中
//搜50次-100次的所有document集合
TopDocs topDocs = searcher.search(query,10);//先从索引倒排索引表计算,获取前10条结果
System.out.println("查询总条数:"+topDocs.totalHits);
ScoreDoc[] scoreDocs=topDocs.scoreDocs;
//从数组里,解析前10条docId值,通过id读取所有document数据
for (ScoreDoc scoreDoc:scoreDocs){
//元素对象中就包含了一个documentId
int docId = scoreDoc.doc;
Document doc = searcher.doc(docId);
System.out.println("title:"+doc.get("title"));
System.out.println("content:"+doc.get("content"));
System.out.println("publisher:"+doc.get("publisher"));
System.out.println("click:"+doc.get("click"));
}
}