Solr 是一種可供企業使用的、基于 Lucene 的搜尋伺服器,它支援層面搜尋、命中醒目顯示和多種輸出格式。在這篇文章中,我将介紹 Solr 的部署和使用的基本操作,希望能讓初次使用的朋友們少踩一些坑
前言
Solr 是一種可供企業使用的、基于 Lucene 的搜尋伺服器,它支援層面搜尋、命中醒目顯示和多種輸出格式。在這篇文章中,我将介紹 Solr 的部署和使用的基本操作,希望能讓初次使用的朋友們少踩一些坑。
下載下傳位址:https://lucene.apache.org/solr/downloads.html
本文中使用的 Solr 版本:7.7.2,因為我是用的是 Windows 系統,是以主要介紹的是 Windows 下的部署方法。
安裝
Solr 内置了 Jetty,是以不需要任何安裝任何 Web 容器即可運作。直接通過指令行就可以啟動。
啟動 Solr:
.\solr.cmd start
停止 Solr:
.\solr.cmd stop -all
建立 Core
首先在
server\solr
檔案夾中建立一個新的目錄,然後将
server\solr\configsets\_default
下的
conf
目錄複制到剛剛建立的檔案夾。
在浏覽器中打開
http://localhost:8983/solr/
點選左側的
Core Admin
添加 Core。
name
和
instanceDir
都改成剛剛建立的目錄名稱。
建立好之後即可在左側的
Core Selector
中找到這個 Core。
現在一個 Core 就建立好了,在 Core 的面闆裡可以對其進行一些基本操作。
Solr 的 Api 是支援通過調用接口添加資料的,但是在實際使用中我們都是從資料庫中同步資料,是以我們需要為 Solr 配置資料源。
在
solrconfig.xml
檔案中找到如下内容:
<!-- Request Handlers
http://wiki.apache.org/solr/SolrRequestHandler
Incoming queries will be dispatched to a specific handler by name
based on the path specified in the request.
If a Request Handler is declared with startup="lazy", then it will
not be initialized until the first request that uses it.
-->
添加一個
requestHandler
節點:
<requestHandler name="/dataimport" class="solr.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>
data-config.xml 檔案的大緻結構如下:
稍後會對 data-config.xml 檔案進行詳細介紹。
配置資料源
使用 SQL Server 資料源
從微軟官網下載下傳 SQL Server 的 Microsoft SQL Server JDBC 驅動程式 4.1 驅動,複制到
server\solr-webapp\webapp\WEB-INF\lib
目錄下。
這裡需要注意的是把在下載下傳的檔案重命名為
sqljdbc4.jar
,我之前沒有改名死活加載不上。
使用
com.microsoft.sqlserver.jdbc.SQLServerDriver
驅動配置資料源:
<dataSource name="postData" driver="com.microsoft.sqlserver.jdbc.SQLServerDriver" url="jdbc:sqlserver://127.0.0.1:1433;SelectMethod=Cursor;DatabaseName=post;useLOBs=false;loginTimeout=60" user="charlestest" password="12345678" />
使用 MySQL 資料源
下載下傳:mysql-connector-java-6.0.6.jar 複制到
server\solr-webapp\webapp\WEB-INF\lib
目錄下。
從
dist
目錄複制
solr-dataimporthandler-7.7.2.jar
到
server/solr-webapp/webapp/WEB-INF/lib
中。
配置
data-config.xml
:
<dataConfig>
<dataSource name="postsData" type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/posts?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC" user="root" password="12345678" batchSize="-1" />
<document name="posts">
<entity name="Post" dataSource="postData" pk="Id" transformer="DateFormatTransformer,HTMLStripTransformer" rootEntity="true" query="SELECT Id, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count
FROM wp_posts"
deltaQuery="SELECT Id, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count
FROM wp_posts post_modified >\'${dataimporter.last_index_time}\' "
>
<field column="Id" />
<field column="post_author" />
<field column="post_date" dateTimeFormat=\'yyyy-MM-dd HH:mm:ss\'/>
<field column="post_date_gmt" dateTimeFormat=\'yyyy-MM-dd HH:mm:ss\'/>
<field column="post_content" />
<field column="post_title" />
<field column="post_excerpt" />
<field column="post_status" />
<field column="comment_status" />
<field column="ping_status" />
<field column="post_password" />
<field column="post_name" />
<field column="to_ping" />
<field column="pinged" />
<field column="post_modified" dateTimeFormat=\'yyyy-MM-dd HH:mm:ss\'/>
<field column="post_modified_gmt" dateTimeFormat=\'yyyy-MM-dd HH:mm:ss\'/>
<field column="post_content_filtered" />
<field column="post_parent" />
<field column="guid" />
<field column="menu_order" />
<field column="post_type" />
<field column="post_mime_type" />
<field column="comment_count" />
<entity name="PostAuthor" dataSource="authordata" pk="Id" query="SELECT Id, user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_activation_key, user_status, display_name
FROM wp_users where id=${Post.post_author}">
<field column="Id" />
<field column="user_login"/>
<field column="user_pass"/>
<field column="user_nicename"/>
<field column="user_email"/>
<field column="user_url"/>
<field column="user_registered"/>
<field column="user_activation_key"/>
<field column="user_status"/>
<field column="display_name"/>
</entity>
</entity>
</document>
</dataConfig>
entity 中的一些常用屬性:
- query:查詢隻對第一次全量導入有作用,對增量同步不起作用。
- deltaQuery:的意思是,查詢出所有經過修改的記錄的 Id 可能是修改操作,添加操作,删除操作産生的(此查詢隻對增量導入起作用,而且隻能傳回 Id 值)
- deletedPkQuery:此操作值查詢那些資料庫裡僞删除的資料的 Id、solr 通過它來删除索引裡面對應的資料(此查詢隻對增量導入起作用,而且隻能傳回 Id 值)。
- deltaImportQuery:是擷取以上兩步的 Id,然後把其全部資料擷取,根據擷取的資料對索引庫進行更新操作,可能是删除,添加,修改(此查詢隻對增量導入起作用,可以傳回多個字段的值,一般情況下,都是傳回所有字段的列)。
- parentDeltaQuery:從本 entity 中的 deltaquery 中取得參數。
dataSource 中 batchSize 屬性的作用是可以在批量導入的時候限制連接配接數量。
配置完成後重新加載一下 Core。
中文分詞
将
contrib\analysis-extras\lucene-libs
目錄中的
lucene-analyzers-smartcn-7.7.2.jar
複制到
server\solr-webapp\webapp\WEB-INF\lib
目錄下,否則會報錯。
在
managed-shchema
中添加如下代碼:
<!-- 配置中文分詞器 -->
<fieldType name="text_cn" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory" />
</analyzer>
<analyzer type="query">
<tokenizer class="org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory" />
</analyzer>
</fieldType>
把需要使用中文分詞的字段類型設定成
text_cn
:
<field name="Remark" type="text_cn" indexed="true" stored="true" multiValued="false"/>
主從部署
Solr 複制模式,是一種在分布式環境下用于同步主從伺服器的一種實作方式,因之前提到的基于 rsync 的 SOLR 不同方式部署成本過高,被 Solr 1.4 版本所替換,取而代之的就是基于 HTTP 協定的索引檔案傳輸機制,該方式部署簡單,隻需配置一個檔案即可。Solr 索引同步的是 Core 對 Core,以 Core 為基本同步單元。
主伺服器
solrconfig.xml
配置:
<requestHandler name="/replication" class="solr.ReplicationHandler">
<lst name="master">
<!-- 執行 commit 操作後進行 replicate 操作同樣的設定\'startup\', \'commit\', \'optimize\'-->
<str name="replicateAfter">commit</str>
<!-- 執行 startup 操作後進行 replicate 操作 -->
<str name="replicateAfter">startup</str>
<!-- 複制索引時也同步以下配置檔案 -->
<str name="confFiles">schema.xml,stopwords.txt</str>
<!-- 每次 commit 之後,保留增量索引的周期時間,這裡設定為 5 分鐘。 -->
<str name="commitReserveDuration">00:05:00</str>
<!-- 驗證資訊,由使用者自定義使用者名-->
<!-- <str name="httpBasicAuthUser">root</str> -->
<!-- 驗證資訊,由使用者自定義密碼 -->
<!-- <str name="httpBasicAuthPassword">password</str> -->
</lst>
<!--
<lst name="slave">
<str name="masterUrl">http://your-master-hostname:8983/solr</str>
<str name="pollInterval">00:00:60</str>
</lst>
-->
</requestHandler>
從伺服器
solrconfig.xml
配置:
<requestHandler name="/replication" class="solr.ReplicationHandler">
<lst name="slave">
<!-- 主伺服器的同步位址 -->
<str name="masterUrl">http://192.168.1.135/solr/posts</str>
<!-- 從伺服器同步間隔,即每隔多長時間同步一次主伺服器 -->
<str name="pollInterval">00:00:60</str>
<!-- 壓縮機制,來傳輸索引,可選 internal|external,internal:内網,external:外網 -->
<str name="compression">internal</str>
<!-- 設定連接配接逾時(機關:毫秒) -->
<str name="httpConnTimeout">50000</str>
<!-- 如果設定同步索引檔案過大,則應适當提高此值。(機關:毫秒) -->
<str name="httpReadTimeout">500000</str>
<!-- 驗證使用者名,需要和 master 伺服器一緻 -->
<!-- <str name="httpBasicAuthUser">root</str> -->
<!-- 驗證密碼,需要和 master 伺服器一緻 -->
<!-- <str name="httpBasicAuthPassword">password</str> -->
</lst>
</requestHandler>
Solr 主從同步是通過 Slave 周期性輪詢來檢查 Master 的版本,如果 Master 有新版本的索引檔案,Slave 就開始同步複制。
- 1、Slave 發出一個 filelist 指令來收集檔案清單。這個指令将傳回一系列中繼資料(size、lastmodified、alias 等資訊)。
- 2、Slave 檢視它本地是否有這些檔案,然後它會開始下載下傳缺失的檔案(使用指令 filecontent)。如果與 Master 連接配接失敗,就會重新連接配接,如果重試 5 次還是沒有成功,就會 Slave 停止同步。
- 3、檔案被同步到了一個臨時目錄(
格式的檔案夾名稱,例如:index.20190614133600008)。舊的索引檔案還存放在原來的檔案夾中,同步過程中出錯不會影響到 Slave,如果同步過程中有請求通路,Slave 會使用舊的索引。index.時間戳
- 4、當同步結束後,Slave 就會删除舊的索引檔案使用最新的索引。
我們項目中 6.7G 的索引檔案(279 萬條記錄),大概隻用了 12 分鐘左右就同步完成了,平均每秒的同步速度大約在 10M 左右。
注意事項: 如果主從的資料源配置的不一緻,很可能導緻從伺服器無法同步索引資料。
在項目中使用 Solr
在 Java 項目中使用 Solr
SolrJ 是 Solr 的官方用戶端,文檔位址:https://lucene.apache.org/solr/7_7_2/solr-solrj/。
使用 maven 添加:
<!-- https://mvnrepository.com/artifact/org.apache.solr/solr-solrj -->
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>7.7.2</version>
</dependency>
查詢索引文檔:
String keyword = "蘋果";
Map<String, String> queryParamMap = new HashMap<String, String>();
queryParamMap.put("q", "*:*");
queryParamMap.put("fq", keyword);
MapSolrParams queryParams = new MapSolrParams(queryParamMap);
QueryResponse queryResponse = client.query("posts", queryParams);
SolrDocumentList results = queryResponse.getResults();
添加和更新索引文檔:
// 通過 屬性 添加到索引中
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "10000");
doc.addField("post_title", "test-title");
doc.addField("post_name", "test-name");
doc.addField("post_excerpt", "test-excerpt");
doc.addField("post_content", "test-content");
doc.addField("post_date", "2019-06-18 14:56:55");
client.add("posts", doc);
// 通過 Bean 添加到索引中
Post post = new Post();
post.setId(10001);
post.setPost_title("test-title-10001");
post.setPost_name("test-name");
post.setPost_excerpt("test-excerpt");
post.setPost_content("test-content");
post.setPost_date(new Date());
client.addBean("posts", post);
client.commit("posts");
具體代碼可以參考我 GitHub 中的示例,這裡就不詳細列出了。
在 DotNet 項目中使用 Solr
SolrNet:https://github.com/mausch/SolrNet
通過 Nuget 添加 SolrNet:
Install-Package SolrNet
首先定義一個索引對象
PostDoc
:
/// <summary>
/// 文章 doc。
/// </summary>
[Serializable]
public class PostDoc
{
[SolrUniqueKey("id")]
public int Id { get; set; }
[SolrField("post_title")]
public string Title { get; set; }
[SolrField("post_name")]
public string Name { get; set; }
[SolrField("post_excerpt")]
public string Excerpt { get; set; }
[SolrField("post_content")]
public string Content { get; set; }
[SolrField("post_date")]
public DateTime PostDate { get; set; }
}
在項目的
Startup
類中初始化 SolrNet:
SolrNet.Startup.Init<PostDoc>("http://localhost:8983/solr/posts");
添加或更新文檔操作:
// 同步添加文檔
solr.Add(
new PostDoc()
{
Id = 30001,
Name = "This SolrNet Name",
Title = "This SolrNet Title",
Excerpt = "This SolrNet Excerpt",
Content = "This SolrNet Content 30001",
PostDate = DateTime.Now
}
);
// 異步添加文檔(更新)
await solr.AddAsync(
new PostDoc()
{
Id = 30001,
Name = "This SolrNet Name",
Title = "This SolrNet Title",
Excerpt = "This SolrNet Excerpt",
Content = "This SolrNet Content Updated 30001",
PostDate = DateTime.Now
}
);
// 送出
ResponseHeader responseHeader = await solr.CommitAsync();
删除文檔操作:
// 使用文檔 Id 删除
await solr.DeleteAsync("300001");
// 直接删除文檔
await solr.DeleteAsync(new PostDoc()
{
Id = 30002,
Name = "This SolrNet Name",
Title = "This SolrNet Title",
Excerpt = "This SolrNet Excerpt",
Content = "This SolrNet Content 30002",
PostDate = DateTime.Now
});
// 送出
ResponseHeader responseHeader = await solr.CommitAsync();
搜尋并對結果進行排序,在不傳入分頁參數的情況下 SolrNet 會傳回所有滿足條件的結果。
// 排序
ICollection<SortOrder> sortOrders = new List<SortOrder>() {
new SortOrder("id", Order.DESC)
};
// 使用查詢條件并排序
SolrQueryResults<PostDoc> docs = await solr.QueryAsync("post_title:索尼", sortOrders);
使用字段篩選的另一種方式:
// 使用條件查詢
SolrQueryResults<PostDoc> posts = solr.Query(new SolrQueryByField("id", "30000"));
分頁查詢并對高亮關鍵字:
SolrQuery solrQuery = new SolrQuery("蘋果");
QueryOptions queryOptions = new QueryOptions
{
// 高亮關鍵字
Highlight = new HighlightingParameters
{
Fields = new List<string> { "post_title" },
BeforeTerm = "<font color=\'red\'><b>",
AfterTerm = "</b></font>"
},
// 分頁
StartOrCursor = new StartOrCursor.Start(pageIndex * pageSize),
Rows = pageSize
};
SolrQueryResults<PostDoc> docs = await solr.QueryAsync(solrQuery, queryOptions);
var highlights = docs.Highlights;
高亮關鍵字需要在傳回結果中單獨擷取,
docs.Highlights
是一個
IDictionary<string, HighlightedSnippets>
對象,每個
key
對應文檔的
id
,
HighlightedSnippets
中也是一個
Dictionary
,存儲高亮處理後的字段和内容。
在 Python 項目中使用 Solr
PySolr:https://github.com/django-haystack/pysolr
使用
pip
安裝 pysolr:
pip install pysolr
簡單的操作:
# -*- coding: utf-8 -*-
import pysolr
SOLR_URL = \'http://localhost:8983/solr/posts\'
def add():
"""
添加
"""
result = solr.add([
{
\'id\': \'20000\',
\'post_title\': \'test-title-20000\',
\'post_name\': \'test-name-20000\',
\'post_excerpt\': \'test-excerpt-20000\',
\'post_content\': \'test-content-20000\',
\'post_date\': \'2019-06-18 14:56:55\',
},
{
\'id\': \'20001\',
\'post_title\': \'test-title-20001\',
\'post_name\': \'test-name-20001\',
\'post_excerpt\': \'test-excerpt-20001\',
\'post_content\': \'test-content-20001\',
\'post_date\': \'2019-06-18 14:56:55\',
}
])
solr.commit()
results = solr.search(q=\'id: 20001\')
print(results.docs)
def delete():
"""
删除
"""
solr.delete(q=\'id: 20001\')
solr.commit()
results = solr.search(q=\'id: 20001\')
print(results.docs)
def update():
"""
更新
"""
solr.add([
{
\'id\': \'20000\',
\'post_title\': \'test-title-updated\',
\'post_name\': \'test-name-updated\',
\'post_excerpt\': \'test-excerpt-updated\',
\'post_content\': \'test-content-updated\',
\'post_date\': \'2019-06-18 15:00:00\',
}
])
solr.commit()
results = solr.search(q=\'id: 20000\')
print(results.docs)
def query():
"""
查詢
"""
results = solr.search(\'蘋果\')
print(results.docs)
if __name__ == "__main__":
solr = pysolr.Solr(SOLR_URL)
add()
delete()
update()
query()
需要注意的是在使用
solr.add()
和
solr.delete
方法以後需要執行一下
solr.commit()
方法,否則文檔的變更不會送出。
如果想擷取添加或更新是否成功可以通過判斷
solr.commit()
方法傳回結果,
solr.commit()
方法的傳回結果是一個 xml 字元串:
<xml version="1.0" encoding="UTF-8">
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">44</int>
</lst>
</response>
</xml>
status
的值如果是 0 就表示送出成功了。
總結
通過簡單使用和測試,就會發現搜尋結果并不是很精準,比如搜尋“微軟”這個關鍵字,搜尋出來的資料中有完全不包含這個關鍵字的内容,是以要想讓搜尋結果更加準确就必須對 Sorl 進行調優,Solr 中還有很多進階的用法,例如設定字段的權重、自定義中文分詞詞庫等等,有機會我會專門寫一篇這樣的文章來介紹這些功能。
我在
sql
目錄裡提供了資料庫腳本,友善大家建立測試資料,資料是以前做的一個小站從網上抓取過來的科技新聞。
項目位址:https://github.com/weisenzcharles/SolrExample