首先,幫忙點擊一下我的網站http://www.wenzhihuai.com/ 。謝謝啊,如果可以,GitHub上麻煩給個star,以后面試能講講這個項目,GitHub地址https://github.com/Zephery/newblog 。
Lucene的整體架構
搜索引擎的幾個重要概念:
-
倒排索引:將文檔中的詞作為關鍵字,建立詞與文檔的映射關系,通過對倒排索引的檢索,可以根據詞快速獲取包含這個詞的文檔列表。倒排索引一般需要對句子做去除停用詞。
-
停用詞:在一段句子中,去掉之后對句子的表達意向沒有印象的詞語,如“非常”、“如果”,中文中主要包括冠詞,副詞等。
-
排序:搜索引擎在對一個關鍵詞進行搜索時,可能會命中許多文檔,這個時候,搜索引擎就需要快速的查找的用戶所需要的文檔,因此,相關度大的結果需要進行排序,這個設計到搜索引擎的相關度算法。
Lucene中的幾個概念
- 文檔(Document):文檔是一系列域的組合,文檔的域則代表一系列域文檔相關的內容。
- 域(Field):每個文檔可以包含一個或者多個不同名稱的域。
- 詞(Term):Term是搜索的基本單元,與Field相對應,包含了搜索的域的名稱和關鍵詞。
- 查詢(Query):一系列Term的條件組合,成為TermQuery,但也有可能是短語查詢等。
- 分詞器(Analyzer):主要是用來做分詞以及去除停用詞的處理。
索引的建立
lucene在本網站的使用:
- 搜索 2. 自動分詞
一、搜索
注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一樣。首先需要導入lucene所使用的包。使用maven:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId><!--lucene核心-->
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId><!--分詞器-->
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId><!--中文分詞器-->
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId><!--格式化-->
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
<version>${lucene.version}</version>
</dependency>
- 構建索引
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存儲的位置
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//簡單的分詞器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(dir, config);
Document doc = new Document();
doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //對標題做索引
doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//對文章內容做索引
writer.addDocument(doc);
writer.close();
- 更新與刪除
IndexWriter writer = getWriter();
Document doc = new Document();
doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc); //更新索引
writer.close();
- 查詢
private static void search_index(String keyword) {
try {
Directory dir = FSDirectory.open(Paths.get("blog_index")); //獲取要查詢的路徑,也就是索引所在的位置
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
QueryParser parser = new QueryParser("content", analyzer); //查詢解析器
Query query = parser.parse(keyword); //通過解析要查詢的String,獲取查詢對象
TopDocs docs = searcher.search(query, 10);//開始查詢,查詢前10條數據,將記錄保存在docs中,
for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每條查詢結果
Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相當於docID,根據這個docID來獲取文檔
System.out.println(doc.get("title")); //fullPath是剛剛建立索引的時候我們定義的一個字段
}
reader.close();
} catch (IOException | ParseException e) {
logger.error(e.toString());
}
}
- 高亮
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
highlighter.setTextFragmenter(fragmenter);
for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每條查詢結果
Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相當於docID,根據這個docID來獲取文檔
String title = doc.get("title");
TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
String hTitle = highlighter.getBestFragment(tokenStream, title);
System.out.println(hTitle);
}
結果
<b><font color='red'>Java</font></b>堆.棧和常量池 筆記
- 分頁
目前lucene分頁的方式主要有兩種:
(1). 每次都全部查詢,然后通過截取獲得所需要的記錄。由於采用了分詞與倒排索引,所有速度是足夠快的,但是在數據量過大的時候,占用內存過大,容易造成內存溢出
(2). 使用searchAfter把數據保存在緩存里面,然后再去取。這種方式對大量的數據友好,但是當數據量比較小的時候,速度會相對慢。
lucene中使用searchafter來篩選順序
ScoreDoc lastBottom = null;//相當於pageSize
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
QueryParser parser1 = new QueryParser("title", analyzer);//對文章標題進行搜索
Query query1 = parser1.parse(q);
booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits); //lastBottom(pageSize),pagehits(pagenum)
- 使用效果
全部代碼放在這里,代碼寫的不太好,光從代碼規范上就不咋地。在網頁上的使用效果如下:
二、lucene自動補全
百度、谷歌等在輸入文字的時候會彈出補全框,如下圖:
在搭建lucene自動補全的時候,也有考慮過使用SQL語句中使用like來進行,主要還是like對數據庫壓力會大,而且相關度沒有lucene的高。主要使用了官方suggest庫以及autocompelte.js這個插件。
suggest的原理看這,以及索引結構看這。
使用:
- 導入maven包
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-suggest</artifactId>
<version>6.6.0</version>
</dependency>
- 如果想將結果反序列化,聲明實體類的時候要加上:
public class Blog implements Serializable {
- 實現InputIterator接口
InputIterator的幾個方法:
long weight():返回的權重值,大小會影響排序,默認是1L
BytesRef payload():對某個對象進行序列化
boolean hasPayloads():是否有設置payload信息
Setcontexts():存入context,context里可以是任意的自定義數據,一般用於數據過濾
boolean hasContexts():判斷是否有下一個,默認為false
public class BlogIterator implements InputIterator {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
private Iterator<Blog> blogIterator;
private Blog currentBlog;
public BlogIterator(Iterator<Blog> blogIterator) {
this.blogIterator = blogIterator;
}
@Override
public boolean hasContexts() {
return true;
}
@Override
public boolean hasPayloads() {
return true;
}
public Comparator<BytesRef> getComparator() {
return null;
}
@Override
public BytesRef next() {
if (blogIterator.hasNext()) {
currentBlog = blogIterator.next();
try {
//返回當前Project的name值,把blog類的name屬性值作為key
return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
} catch (Exception e) {
e.printStackTrace();
return null;
}
} else {
return null;
}
}
/**
* 將Blog對象序列化存入payload
* 可以只將所需要的字段存入payload,這里對整個實體類進行序列化,方便以后需求,不建議采用這種方法
*/
@Override
public BytesRef payload() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(currentBlog);
out.close();
BytesRef bytesRef = new BytesRef(bos.toByteArray());
return bytesRef;
} catch (IOException e) {
logger.error("", e);
return null;
}
}
/**
* 文章標題
*/
@Override
public Set<BytesRef> contexts() {
try {
Set<BytesRef> regions = new HashSet<BytesRef>();
regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
return regions;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Couldn't convert to UTF-8");
}
}
/**
* 返回權重值,這個值會影響排序
* 這里以產品的銷售量作為權重值,weight值即最終返回的熱詞列表里每個熱詞的權重值
*/
@Override
public long weight() {
return currentBlog.getHits(); //change to hits
}
}
- ajax 建立索引
/**
* ajax建立索引
*/
@Override
public void ajaxbuild() {
try {
Directory dir = FSDirectory.open(Paths.get("autocomplete"));
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
//創建Blog測試數據
List<Blog> blogs = blogMapper.getAllBlog();
suggester.build(new BlogIterator(blogs.iterator()));
} catch (IOException e) {
System.err.println("Error!");
}
}
- 查找
因為有些文章的標題是一樣的,先對list排序,將標題短的放前面,長的放后面,然后使用LinkHashSet來存儲。
@Override
public Set<String> ajaxsearch(String keyword) {
try {
Directory dir = FSDirectory.open(Paths.get("autocomplete"));
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
List<String> list = lookup(suggester, keyword);
list.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.length() > o2.length()) {
return 1;
} else {
return -1;
}
}
});
Set<String> set = new LinkedHashSet<>();
for (String string : list) {
set.add(string);
}
ssubSet(set, 7);
return set;
} catch (IOException e) {
System.err.println("Error!");
return null;
}
}
- controller層
@RequestMapping("ajaxsearch")
public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
String keyword = request.getParameter("keyword");
if (StringUtils.isEmpty(keyword)) {
return;
}
Set<String> set = blogService.ajaxsearch(keyword);
Gson gson = new Gson();
response.getWriter().write(gson.toJson(set));//返回的數據使用json
}
- ajax來提交請求
autocomplete.js源代碼與介紹:https://github.com/xdan/autocomplete
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
<script type="text/javascript">
/******************** remote start **********************/
$('#remote_input').autocomplete({
source: [
{
url: "ajaxsearch.html?keyword=%QUERY%",
type: 'remote'
}
]
});
/********************* remote end ********************/
</script>
- 效果:
歡迎訪問我的個人網站
參考:
https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/