Solr 7 部署與使用入門踩坑全記錄

前言

Solr 是一種可供企業使用的、基於 Lucene 的搜索服務器,它支持層面搜索、命中醒目顯示和多種輸出格式。在這篇文章中,我將介紹 Solr 的部署和使用的基本操做,但願能讓初次使用的朋友們少踩一些坑。html

下載地址:https://lucene.apache.org/solr/downloads.htmljava

本文中使用的 Solr 版本:7.7.2,由於我是用的是 Windows 系統,因此主要介紹的是 Windows 下的部署方法。python

安裝

Solr 內置了 Jetty,因此不須要任何安裝任何 Web 容器便可運行。直接經過命令行就能夠啓動。mysql

啓動 Solr:git

.\solr.cmd start

中止 Solr:github

.\solr.cmd stop -all

建立 Core

首先在 server\solr 文件夾中建立一個新的目錄,而後將 server\solr\configsets\_default 下的 conf 目錄複製到剛剛建立的文件夾。web

在瀏覽器中打開 http://localhost:8983/solr/ 點擊左側的 Core Admin 添加 Core。sql

nameinstanceDir 都改爲剛剛建立的目錄名稱。shell

建立好以後便可在左側的 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.jarserver/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&amp;useJDBCCompliantTimezoneShift=true&amp;useLegacyDatetimeCode=false&amp;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 &gt;'${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 就開始同步複製。

  • 一、Slave 發出一個 filelist 命令來收集文件列表。這個命令將返回一系列元數據(size、lastmodified、alias 等信息)。
  • 二、Slave 查看它本地是否有這些文件,而後它會開始下載缺失的文件(使用命令 filecontent)。若是與 Master 鏈接失敗,就會從新鏈接,若是重試 5 次仍是沒有成功,就會 Slave 中止同步。
  • 三、文件被同步到了一個臨時目錄(index.時間戳 格式的文件夾名稱,例如:index.20190614133600008)。舊的索引文件還存放在原來的文件夾中,同步過程當中出錯不會影響到 Slave,若是同步過程當中有請求訪問,Slave 會使用舊的索引。
  • 四、當同步結束後,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 對應文檔的 idHighlightedSnippets 中也是一個 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

相關文章
相關標籤/搜索