信息檢索模型
信息檢索模型最重要的概念就是倒排索引
,倒排索引是搜索引擎中常見的索引方法,用來存儲在全文搜索下某個單詞在一個文檔中存儲位置的映射。通過倒排索引,我們輸入一個關鍵詞,可以非常快地獲取包含這個關鍵詞的文檔列表。
Lucene
Lucene 是一個基於 Java 的全文信息檢索工具包,它不是一個完整的搜索應用程序,而是爲你的應用程序提供索引和搜索功能。Lucene 目前是 Apache Jakarta 家族中的一個開源項目。也是目前最爲流行的基於 Java 開源全文檢索工具包。
Elasticsearch 就是基於 Lucene 的。
demo
官網:https://lucene.apache.org/core/downloads.html
可以使用國內鏡像下載:https://mirrors.tuna.tsinghua.edu.cn/apache/lucene/java/8.5.2/lucene-8.5.2.zip
下面是 Java 代碼的簡單例子,Maven 配置文件:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>8.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>8.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>8.5.2</version>
</dependency>
測試分詞
注意下面使用的 SmartChineseAnalyzer 是包 lucene-analyzers-smartcn。
@Test
public void testAnalyzer() throws IOException {
String chinese = "中華人民共和國簡稱中國,是一個有13億人口的國家";
Analyzer analyzer = new SmartChineseAnalyzer();
TokenStream tokenStream = analyzer.tokenStream(chinese, new StringReader(chinese));
tokenStream.reset();
CharTermAttribute attribute = tokenStream.getAttribute(CharTermAttribute.class);
System.out.println("分詞結果:");
while (tokenStream.incrementToken()) {
System.out.print(attribute.toString() + "|");
}
analyzer.close();
}
程序輸出:
分詞結果:
中華人民共和國|簡稱|中國|是|一個|有|13|億|人口|的|國家|
測試索引
下面程序創建了3個包含 id, title, content 的文檔,其中每個類型都是 FieldType,使用 SmartChineseAnalyzer。索引目錄是 web 根目錄下的 indexDir 文件夾。
@Test
public void testIndex() throws IOException {
List<String> titleList = Lists.newArrayList("中國房企洛杉磯醜聞:百萬美元行賄案遭曝光", "2025年之前美國不會退出WTO了",
"特朗普退出總統競選?");
List<String> contentList = Lists.newArrayList(
"據調查,惠澤爾從中國房企手裏收取了超過150萬美元的現金賄賂,合人民幣超過1000萬元。",
"美國特朗普政府上臺以來,每隔幾月,便總要傳出有關“美國要退出世貿組織(WTO)”的消息。那麼究竟美國能不能退出WTO?",
"“共和黨的操盤手首次提出了這樣的可能性”,即川普總統可能會退出2020年總統競選");
Path indexPath = Paths.get("indexDir");
Directory dir = FSDirectory.open(indexPath);
// 設置新聞ID索引並存儲
FieldType idType = new FieldType();
idType.setIndexOptions(IndexOptions.DOCS);
idType.setStored(true);
// 設置新聞標題索引文檔、此項頻率、位移信息、偏移量,存儲並詞條化
FieldType titleType = new FieldType();
titleType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
titleType.setStored(true);
titleType.setTokenized(true);
FieldType contentType = new FieldType();
contentType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
contentType.setStored(true);
contentType.setTokenized(true);
contentType.setStoreTermVectors(true);
contentType.setStoreTermVectorOffsets(true);
contentType.setStoreTermVectorPayloads(true);
contentType.setStoreTermVectorPositions(true);
Analyzer analyzer = new SmartChineseAnalyzer();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
IndexWriter indexWriter = new IndexWriter(dir, indexWriterConfig);
for (int i = 0; i < titleList.size(); i++) {
Document doc = new Document();
doc.add(new Field("id", Integer.toString(i + 1), idType));
doc.add(new Field("title", titleList.get(i), titleType));
doc.add(new Field("content", contentList.get(i), contentType));
indexWriter.addDocument(doc);
}
indexWriter.commit();
indexWriter.close();
dir.close();
}
在運行代碼後,會在根目錄生成 indexDir 文件夾(代碼中指定),如下圖所示。
搜索索引
@Test
public void testSearch() throws Exception {
Path indexPath = Paths.get("indexDir");
Directory dir = FSDirectory.open(indexPath);
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
Analyzer analyzer = new SmartChineseAnalyzer();
QueryParser parser = new QueryParser("title", analyzer);
parser.setDefaultOperator(QueryParser.Operator.AND);
Query query = parser.parse("房企");
System.out.println("query : " + query.toString());
TopDocs topDocs = searcher.search(query, 10);
for (ScoreDoc sd : topDocs.scoreDocs) {
Document doc = searcher.doc(sd.doc);
System.out.println("docId: " + sd.doc);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
System.out.println("content: " + doc.get("content"));
System.out.println("文檔評分:" + sd.score);
}
}
會搜出關於房企的信息。
Elasticsearch
全文搜索屬於最常見的需求,開源的 Elasticsearch (以下簡稱 Elastic)是目前全文搜索引擎的首選。
它可以快速地儲存、搜索和分析海量數據。維基百科、Stack Overflow、Github 都採用它。
Elastic 的底層是開源庫 Lucene。但是,你沒法直接用 Lucene,必須自己寫代碼去調用它的接口。Elastic 是 Lucene 的封裝,提供了 REST API 的操作接口,開箱即用。
入門教程推薦阮一峯的《全文搜索引擎 Elasticsearch 入門教程》
官網是:https://www.elastic.co/cn/,上面內容很全面,感覺直接看官網最好。
Elastic Stack
原來是通過 Logstash 進行日誌收集與解析,Elasticsearch 作爲搜索引擎,Kibana 作爲可視化分析平臺。但是 Logstash 有CPU和內存性能問題,官方開發了 Beats 數據採集工具。本文通過一個例子使用 Java 直接向 Elasticsearch 發送消息,並搭建 Kibana 數據可視化查詢。
Docker 搭建 Elasticsearch
說明:經過試驗,本文不使用最新版本,而是使用 Elasticsearch 6.8.4 版本,因爲 Spring boot data 2.3 集成的版本就是 6.8.4,同時 Kibana 也要和 Elasticsearch 版本完全一致,否則會出各種奇葩問題。
docker 拉取 6.8.4 版本鏡像:
docker pull elasticsearch:6.8.4
啓動鏡像:
docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:6.8.4
這裏使用簡單模式,9200是 HTTP rest 協議,9300 是 tcp 協議。啓動完成後,可以在瀏覽器中輸入網址 0.0.0.0:9200,返回一下內容說明啓動成功:
{
"name": "_jhdsik",
"cluster_name": "docker-cluster",
"cluster_uuid": "mpaTnrRaSY2_e3LFPz4QXw",
"version": {
"number": "6.8.4",
"build_flavor": "default",
"build_type": "docker",
"build_hash": "bca0c8d",
"build_date": "2019-10-16T06:19:49.319352Z",
"build_snapshot": false,
"lucene_version": "7.7.2",
"minimum_wire_compatibility_version": "5.6.0",
"minimum_index_compatibility_version": "5.0.0"
},
"tagline": "You Know, for Search"
}
如果要看 log,可以使用命令:
docker logs -f 44afc4738685
其中 44afc4738685 是 CONTAINER ID(可通過 docker ps 查看)。
修改 Elasticsearch 配置:
docker exec -it epic_beaver /bin/bash
其中 epic_beaver 是我的 docker Elasticsearch 容器名稱。進入 config 目錄修改 elasticsearch.yml 文件。
cluster.name: "docker-cluster"
network.host: 0.0.0.0
# xpack.security.enabled: true
其中 xpack.security.enabled
在設置密碼時使用,暫時不做設置。
修改完配置文件,重啓容器。
Docker 搭建 Kibana
由於 Elasticsearch 使用的是 6.8.4 版本,Kibana 也要使用這個版本。
docker 下載 Kibana 鏡像:
docker pull kebana:6.8.4
修改配置文件(本例是 /root/etc/kibana.yml)
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://0.0.0.0:9200" ]
# xpack.monitoring.ui.container.elasticsearch.enabled: true
注:這裏的 elasticsearch.hosts
配置跟 docker 網絡模式有關,因爲 elasticsearch 和 kibana 是 2 個獨立的 docker 容器,直接設置 http://0.0.0.0:9200 可能不通,需要額外配置。本例使用的是阿里雲的服務器,配置成了公網 IP,配置中改成了 0.0.0.0,讀者需要自行替換。
docker 啓動 Kibana:
docker run -d --restart=always --log-driver json-file --log-opt max-size=100m --log-opt max-file=2 --name kibana -p 5601:5601 -v /root/etc/kibana.yml:/usr/share/kibana/config/kibana.yml kibana:6.8.4
在瀏覽器輸入 0.0.0.0:5601,如果出現下面的界面,則表示啓動成功。
啓動失敗可以通過 docker logs -f kibana_container_id
查看日誌。
demo
本例使用 Spring boot,Maven pom 引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
在 application.yml 中增加:
data:
elasticsearch:
cluster-nodes: 0.0.0.0:9300
cluster-name: docker-cluster
寫一個實體類 EsNewsEntity,indexName 設置爲 news_test:
@Data
@Builder
@Document(indexName = "news_test")
public class EsNewsEntity {
@Id
private String id;
private String title;
private String content;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date time;
}
整體查詢使用 ElasticsearchRepository,裏面有 CRUD 的操作,還有分頁和排序,Spring data 使各種數據查詢有了統一的操作接口,使用起來也很方便。
本例中只是想 ES 中簡單插入一些數據,repository 如下:
public interface EsNewsRepository extends ElasticsearchRepository<EsNewsEntity, String> {
}
插入數據的代碼如下,其中插入了2條 title 爲 “韓國男星” 的文章,1條 title 爲 “特大暴雨”的文章:
@Test
public void testEs() {
EsNewsEntity newsEntity = EsNewsEntity.builder().id("1").title("韓國男星")
.content("我很喜歡大神、金鐘國、光洙").time(DateTime.now().toDate()).build();
esNewsRepository.save(newsEntity);
newsEntity = EsNewsEntity.builder().id("2").title("特大暴雨")
.content("特大暴雨夜襲四川冕寧:山洪摧毀村莊 一家5口遇難").time(DateTime.now().toDate()).build();
esNewsRepository.save(newsEntity);
newsEntity = EsNewsEntity.builder().id("3").title("韓國男星")
.content("韓國男星身材管理多嚴格?金秀賢 Rain有八塊腹肌").time(DateTime.now().toDate()).build();
esNewsRepository.save(newsEntity);
System.out.println("es save ...");
}
運行之後,在 Kibana->Management->Create index pattern,輸入上面 News 的 indexName:news_test,設置 Date 爲時間索引。
之後刷新頁面,可以根據時間搜索新插入的幾條數據。
問題總結
- 本例的 es 和 Kibana 沒有使用密碼登錄,在實際應用過程中 es 可以限制 ip 訪問。我在研究的過程中,設置好 es 和 Kibana 的密碼後,沒法使用 Spring data 直接與 es 通信,暫時放棄。
- es 和 Kibana 的版本一定要一致!
- spring-boot-starter-data-elasticsearch 的 es 版本並不高,最新版本已經是 7.8,但是 spring-boot-starter-data-elasticsearch 集成的仍然是 6.8,需要注意。如果必須使用最新版,需要額外配置。
- Kibana 在分析日誌、數據分析時很強大。
- 本示例是 demo 演示,不要在生產環境中使用。
- 可以直接在騰訊雲、阿里雲上購買 es 服務,不過真心貴……小站點或個人開發者還是自行搭建比較划算。
- 《Lucene Elasticsearch 全文檢索實戰》這本書不建議買,書的內容淺顯,排版和內容問題很多。比如有的代碼分隔符是中文標點,書的前幾章代碼是深色背景,後幾章代碼沒有背景……
Github 地址
公衆號
coding 筆記、點滴記錄,以後的文章也會同步到公衆號(Coding Insight)中,希望大家關注_