1.搜索大數據
1.1 爲什麼要搜索
在當前百萬級數據的面前,如果全部放在同一個表或者某幾個表中,經常搜索數據庫特別模糊搜索會爆嗎?
答案是沒必,但結果可以預測是很慢很慢!
類似:select * from 表名 where 字段名 like ‘%關鍵字%’
例如:select * from article where content like ’%here%’
當關鍵字複雜的話,難道還 like ‘%關鍵字①%’ or like ‘%關鍵字②%’ ?
多個表關聯就更復雜了......
1.2 搜索引擎的必要
百度、google這些搜索引擎可以肯定不是直接搜數據庫的,
例如,在百度搜索“spring boot spring的區別”
從結果可以看出,百度搜索具備以下明顯特點:
1、即使在相關結果數量接近500萬時,也能快速得出結果。
2、搜索的結果不僅僅侷限於完整的“spring boot spring”這一短語,而是將此短語拆分成,“spring”,“springboot ”,“的區別”,“spring區別”等關鍵字。
3、對拆分後的搜索關鍵字進行標紅顯示。
4、…
有沒有發現這裏處理多個單詞的搜索,單詞在這裏是可以分開搜索的,
問題:上述功能,使用大家以前學過的數據庫搜索能夠方便實現嗎?數據庫sql可能實現嗎?
1.3 搜索索引原理
問題的結果,是建立索引,著名的數據庫就是oracle,就是建立大量的索引提高搜索速度。
在實際中,我們可以對數據庫中原始的數據結構(左圖),在業務空閒時事先根據左圖內容,創建新的倒排索引結構的數據區域(右圖)。
用戶有查詢需求時,先訪問倒排索引數據區域(右圖),得出文檔id後,通過文檔id即可快速,準確的通過左圖找到具體的文檔內容。
what the mean?
就是文檔內容有5個,5個數據
我們把數據的內容拆開一個個單詞然後記錄起對應的索引id
然後你輸入匹配單詞的時候,快速找到單詞對應的列表id集合返回給你
這一過程,可以通過我們自己寫程序來實現,也可以借用已經抽象出來的通用開源技術來實現,例如Lucene,
分佈式服務的話solr、elasticsearch。
2.Lucene、Solr、Elasticsearch關係
Lucene:底層的API,工具包
Solr:基於Lucene開發的企業級的搜索引擎產品
Elasticsearch:基於Lucene開發的企業級的搜索引擎產品
2.1 Lucene
Lucene是一套用於全文檢索和搜尋的開源程序庫,由Apache軟件基金會支持和提供
Lucene提供了一個簡單卻強大的應用程序接口(API),能夠做全文索引和搜尋,在Java開發環境裏Lucene是一個成熟的免費開放源代碼工具
ps:全文檢索意思就是對於文章中的每一個單詞都建立索引,然後對這些單詞索引進行排序,當查詢時根據事先建立的索引進行查找。
2.2 關於Elasticsearch 與 Solr 的比較總結
- Solr 利用 Zookeeper 進行分佈式管理,而 Elasticsearch 自身帶有分佈式協調管理功能;
- Solr 支持更多格式的數據,而 Elasticsearch 僅支持json文件格式;
- Solr 官方提供的功能更多,而 Elasticsearch 本身更注重於核心功能,高級功能多有第三方插件提供;
- Solr 在傳統的搜索應用中表現好於 Elasticsearch,但在處理實時搜索應用時效率明顯低於 Elasticsearch。
- Solr 是傳統搜索應用的有力解決方案,但 Elasticsearch 更適用於新興的實時搜索應用。
3.創建索引
文檔Document:數據庫中一條具體的記錄
字段Field:數據庫中的每個字段
目錄Directory:物理存儲位置
寫出器的配置對象:需要分詞器Analyer和lucene的版本(lucene的大版本每次更新都有很大的變化)
3.1 文檔Document
其實這個很理解相當於數據庫裏一條數據。
Id(主鍵) | title(標題) | content(內容) | author(作者) | createTime(創作時間) |
1 | Lucene你好 | Lucene的探索大數據課堂,跟我喊123456789.... | Joker | 2019-9-19 00:00:00 |
這一個整個id爲1的數據就是Document
3.2 字段Field
一個Document中可以有很多個不同的字段,每一個字段都是一個Field類的對象。
一個Document中的字段其類型是不確定的,因此Field類就提供了各種不同的子類,來對應這些不同類型的字段。
這些子類有一些不同的特性:
1)DoubleField、FloatField、IntField、LongField、StringField、TextField這些子類一定會被創建索引,但是不會被分詞,而且不一定會被存儲到文檔列表。要通過構造函數中的參數Store來指定:如果Store.YES代表存儲,Store.NO代表不存儲
2)TextField即創建索引,又會被分詞。(多用於內容)
StringField會創建索引,但是不會被分詞。(多用於主鍵)
3)StoreField一定會被存儲,但是不一定創建索引(多用於不想再去數據庫拿的字段,或者一些重要字段,不想再去找數據庫顯示的,也有用於記錄表名用於知道是哪張表的數據)
如果不分詞,會造成整個字段作爲一個詞條,除非用戶完全匹配,否則搜索不到:
// 創建一個存儲對象
Document doc = new Document();
// 添加字段
doc.add(new StringField("id", id, Field.Store.YES));
doc.add(new TextField("title", title, Field.Store.YES));
doc.add(new TextField("content", content, Field.Store.YES));
問題1:如何確定一個字段是否需要存儲?
如果一個字段要顯示到最終的結果中,那麼一定要存儲,否則就不存儲
問題2:如何確定一個字段是否需要創建索引?
如果要根據這個字段進行搜索,那麼這個字段就必須創建索引。
問題3:如何確定一個字段是否需要分詞?
前提是這個字段首先要創建索引。然後如果這個字段的值是不可分割的,那麼就不需要分詞。例如:ID
3.3 目錄Directory
指定索引要存儲的位置,就是索引文件放去哪裏存放
FSDirectory:文件系統目錄,會把索引庫指向本地磁盤。
特點:速度略慢,但是比較安全,也方便遷移
RAMDirectory:內存目錄,會把索引庫保存在內存。
特點:速度快,但是不安全
3.4 Analyzer
lucene提供很多分詞算法,可以把文檔中的數據按照算法分詞
但是這些分詞器,並沒有合適的中文分詞器,因此一般我們會用第三方提供的分詞器:
庖丁,IK-Analyzer、mmseg4j(MMSegAnalyzer)等
說一下mmseg4j
<dependency>
<groupId>com.chenlb.mmseg4j</groupId>
<artifactId>mmseg4j-core</artifactId>
<version>1.10.0</version>
</dependency>
這個是支持擴展詞典和停用詞典的
代碼是支持的
詞典可以參考源碼放在data文件夾下的dic文件
源碼:https://github.com/chenlb/mmseg4j-core
可以看到分詞的寫法其實很簡單,我們按需增加就好
3.5 IndexWriterConfig(索引寫出器配置類)
設置配置信息
String direct = "D:/test/luceneData";
Directory directory = FSDirectory.open(Paths.get(direct));
IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
// 設置創建索引模式(在原來的索引的基礎上創建或新增)
iwConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
//添加索引,在之前的索引基礎上追加
//iwConfig.setOpenMode(OpenMode.APPEND);
//創建索引,刪除之前的索引
//iwConfig.setOpenMode(OpenMode.CREATE);
3.6 IndexWriter(索引寫出器類)
當分詞算法弄好,配置也好了,就是要把索引找地方記錄保存起來了
/**
* 創建索引並進行存儲
*
* @param title
* @param content
*/
public static void createIndex(String id,String title, String content) throws IOException {
Directory directory = FSDirectory.open(Paths.get(direct));
IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
// 設置創建索引模式(在原來的索引的基礎上創建或新增)
iwConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
IndexWriter iwriter = new IndexWriter(directory, iwConfig);
// 創建一個存儲對象
Document doc = new Document();
// 添加字段
doc.add(new StringField("id", id, Field.Store.YES));
doc.add(new TextField("title", title, Field.Store.YES));
doc.add(new TextField("content", content, Field.Store.YES));
// 新添加一個doc對象
iwriter.addDocument(doc);
// 創建的索引數目
int numDocs = iwriter.numRamDocs();
System.out.println("共索引了: " + numDocs + " 個對象");
// 提交事務
iwriter.commit();
// 關閉事務
iwriter.close();
}
好了,有人問爲什麼有事務?
這個跟數據庫一樣,中間有一個報錯了,可以回滾不保存。
4.大數據查詢(核心)
好了,在這之前我們做了那麼多操作,爲了就是這個時刻把我們想要的結果給拿出來
對應的
查詢解析器:QueryParser(單字段解析器)、MultiFieldQueryParser(多字段的查詢解析器)
查詢對象Query,要查詢的關鍵詞信息:TermQuery(詞條查詢)、WildcardQuery(通配符查詢)、FuzzyQuery(模糊查詢)、NumericRangeQuery(數值範圍查詢)、BooleanQuery(組合查詢,這個大數據常用,將前面都可以一起用,想象一下鏈家房產的搜索數據有多個條件單價啊區域啊學區啊等等)
索引搜索對象IndexSearch(執行搜索功能)
查詢結果對象TopDocs(用於分頁)
4.1 查詢
/**
* 查詢方法
*
* @param text
* @return
* @throws IOException
*/
public static List<Map<String, Object>> search(String text)throws Exception {
// 得到存放索引的位置
Directory directory = FSDirectory.open(Paths.get(direct));
DirectoryReader ireader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(ireader);
// 在content中進行搜索
QueryParser parser = new QueryParser("content", analyzer);
// 搜索含有text的內容
Query query = parser.parse(text);
// 搜索標題和顯示條數(10)
TopDocs tds = searcher.search(query, 10);
// 獲取總條數
System.out.println("本次搜索共找到" + tds.totalHits + "條數據");
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
Map<String, Object> map = null;
// 在內容中查獲找
for (ScoreDoc sd : tds.scoreDocs) {
// 獲取title
String id = searcher.doc(sd.doc).get("id");
// 獲取title
String title = searcher.doc(sd.doc).get("title");
// 獲取content
String content = searcher.doc(sd.doc).get("content");
// 內容添加高亮
QueryParser qp = new QueryParser("content", analyzer);
// 將匹配到的text添加高亮處理
Query q = qp.parse(text);
String HighlightContent = displayHtmlHighlight(q, "content", content);
map = new HashMap<String, Object>();
map.put("id", id);
map.put("title", title);
map.put("content", content);
map.put("highlight", HighlightContent);
list.add(map);
}
return list;
}
/**
* 高亮處理
*
* @param query
* @param fieldName
* @param fieldContent
* @return
*/
public static String displayHtmlHighlight(Query query, String fieldName, String fieldContent)
throws IOException, InvalidTokenOffsetsException {
// 設置高亮標籤,可以自定義,這裏我用html將其顯示爲紅色
SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<font color='red'>", "</font>");
// 評分
QueryScorer scorer = new QueryScorer(query);
// 創建Fragmenter
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
// 高亮分析器
Highlighter highlight = new Highlighter(formatter, scorer);
highlight.setTextFragmenter(fragmenter);
// 調用高亮方法
String str = highlight.getBestFragment(analyzer, fieldName, fieldContent);
return str;
}
4.2 關於排序
// 創建排序對象,需要排序字段SortField,參數:字段的名稱、字段的類型、是否反轉如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
// 搜索
TopDocs topDocs = searcher.search(query, 10,sort);
4.3 分頁
// 實際上Lucene本身不支持分頁。因此我們需要自己進行邏輯分頁。我們要準備分頁參數:
int pageSize = 2;// 每頁條數
int pageNum = 3;// 當前頁碼
int start = (pageNum - 1) * pageSize;// 當前頁的起始條數
int end = start + pageSize;// 當前頁的結束條數(不能包含)
// 搜索數據,查詢0~end條
TopDocs topDocs = searcher.search(query, end);
System.out.println("本次搜索共" + topDocs.totalHits + "條數據");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (int i = start; i < end; i++) {
ScoreDoc scoreDoc = scoreDocs[i];
// 獲取文檔編號
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
}
以上就是lucene的使用
補充一下,這裏使用的lucene版本
<lucene.version>7.7.2</lucene.version>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</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-analyzers-common</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queries</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lucene.version}</version>
</dependency>
最後,謝謝大家堅持到這裏,代碼路上共勉之~!~
下篇會說說solr和elasticsearch的使用和選擇