solr之搜尋引擎
文 / 汝淉
都是個人自己寫的,肯定會有一些不當的地方,還希望海涵,當然指出來多交流
如若轉載,請說明出處…原創不易…謝謝支援
一:solr的介紹
目前市面上流行的搜素引擎有以Solr 和ElasticSearch領銜的等等好幾種…他們的共同點都是能處理千萬以及上億級别的資料搜尋和存儲…功能很強大,資料量越大,就得采用叢集發方式,會讓性能得到很大的提升…
說到Solr 和ElasticSearch不得不提Lucene.一個功能強大的檢索引擎,Solr 和ElasticSearch都是基于Lucene的…是以就有必要提提Lucene.
Lucene: 全稱"全文檢索引擎".是一款高性能的、可拓展的資訊檢索(IR)工具庫,
提供了完整的查詢引擎和索引引擎…他有以下幾種操作類型…
- IndexWriter :負責建立新索引或者打開已有索引,以及向索引中添加、删除或更新被索引文檔的資訊。
- Directory:描述了Lucene索引的存放位置。它是一個抽象類,它的子類負責具體指定索引的存儲路徑。
- Analyzar:負責從被索引文本檔案中提取詞彙單元,并提出剩下的無用資訊。分析器的分析對象為文檔,該文檔包含一些分離的能被索引的域。
- Document:文檔對象代表一些域(Field)的集合。Lucene隻處理從二進制文檔中提取的以Field執行個體出現的文本
- Field:指包含能被索引的文本内容的類。
- IndexSearcher:用于搜尋由IndexWriter類建立的索引。
- Term:搜尋功能的基本單元。與Field對象類似,Term對象包含一對字元串元素:域名和單詞(或域文本值)。
- Query: 查詢類。
- TermQuery:最基本的查詢類型,也是簡單查詢類型之一。用來比對指定域中包含特定項的文檔。
-
TopDocs:一個簡單的指針容器,指針一般指向前N個排名的搜尋結果,搜尋結果即比對查詢條件的文檔。TopDocs會記錄前N個結果中每個結果的int docID(可以用它來恢複文檔)和浮點型分數。
其實,Solr與Lucene 并不是競争對立關系,恰恰相反Solr 依存于Lucene,因為Solr底層的核心技術是使用Lucene 來實作的,Solr和Lucene的本質差別有以下三點:搜尋伺服器,企業級和管理。Lucene本質上是搜尋庫,不是獨立的應用程式,而Solr是。Lucene專注于搜尋底層的建設,而Solr專注于企業應用。Lucene不負責支撐搜尋服務所必須的管理,而Solr負責。
一句話概括 Solr: Solr是Lucene面向企業搜尋應用的擴充。
這些相關标志在後面的Solr界面會有展現.詳細介紹可網上查詢.
以Solr為例.Solr基于Lucene,繼承了Lucene的強大的檢索能力. …下面重點介紹Solr.
Solr:是一個高性能,采用Java5開發,Solr基于Lucene的全文搜尋伺服器。同時對其進行了擴充,提供了比Lucene更為豐富的查詢語言,同時實作了可配置、可擴充并對查詢性能進行了優化,并且提供了一個完善的功能管理界面,是一款非常優秀的全文搜尋引擎。
以下是個人使用見解
個人使用見解: Solr提供了友善的"管理者"操作界面,大大友善了"管理者的操作",使得變的非常靈活 和友善,他有索引建立和索引檢索2個強大的功能…
**索引建立:**他有自己提供的NoSql的索引庫,可以把資料從資料庫增量到索引庫中,對每條資料生成索引,進行存儲…
**索引檢索:**友善查詢,是以才會使千萬級别的資料查詢變的很快…靈活的參數設定和完美的結果展示,使使用者體驗非常好…,上面這2大功能都能在Solr界面展示出來…以圖為例
看的時候, 最左側都是目錄 右側一大片都是query的操作詳情界面
非常友善和靈活的操作界面,後面會逐個詳解各個操作位置的詳情.
二:solr的安裝部署
1. 所需工具solr6.6.2 tomcat8 jdk8 Ik-Analyzer 本地就正常解壓就行.放到tomcat容器中部署.(都可去官網下載下傳)…
solr自己找的下載下傳位址: http://mirrors.shuosc.org/apache/lucene/solr/ (可選擇自己想要的版本)
2. 在解壓後的檔案solr-6.6.2中找到solr-6.6.2\server\solr-webapp下的webapp檔案夾,然後将其複制到tomcat8\webapps目錄下 并起名叫solr
3.把solr-6.6.2\server\lib\ext 下所有的jar包和 solr-6.6.2\dist 下的solr-dataimporthandler-6.4.1.jar、solr-dataimporthandler-extras-6.4.1.jar2個檔案複制到Tomcat8\webapps\solr\WEB-INF\lib路徑中…
4. 建立solr-home 把solr-6.6.2\server目錄下的solr複制到其他目錄地方…(我本地的位置是D:/soft/apache-tomcat-8.0.39/solr-home)…并把該solr起名叫solr-home (索引庫存放地)這是solr核心檔案夾。
5. 到tomcat8\webapps\solr\WEB-INF下的web.xml 在檔案中找到如下内容…取消其注釋.
<env-entry>
<env-entry-name>solr/home</env-entry-name>
<env-entry-value>D:/solr/apache-tomcat-8.0.39/solr-home</env-entry-value>
<env-entry-type>java.lang.String</env-entry-type>
</env-entry>
這裡是配置solr-home的位置.如我的位址是本地的位址D:/solr/apache-tomcat-8.0.39/solr-home
6.将solr-6.6.2\server\lib下的5個metrics開頭的jar包複制到Tomcat8\webapps\solr\WEB-INF\lib下.
7. 将solr-6.6.2\server\resources下的log4j.properties 複制到Tomcat8\webapps\solr\WEB-INF下的classes檔案夾下,此檔案夾手動建立.
8.去掉tomcat的權限…找到Tomcat8\webapps\solr\WEB-INF下的web.xml檔案…注釋掉下面這段文字
<!--
<security-constraint>
<web-resource-collection>
<web-resource-name>Disable TRACE</web-resource-name>
<url-pattern>/</url-pattern>
<http-method>TRACE</http-method>
</web-resource-collection>
<auth-constraint/>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Enable everything but TRACE</web-resource-name>
<url-pattern>/</url-pattern>
<http-method-omission>TRACE</http-method-omission>
</web-resource-collection>
</security-constraint>
-->
然後這就是solr部署安裝成功. 可以啟動用戶端嘗試通路一下.不做其他修改,就是下面這個位址,就是solr的操作界面, http://localhost:10003/solr/index.html (端口号自己tomcat的端口号我的是10003)
三:建立Solr core 之索引庫.(手動操作檔案夾,不是線上直接建立)
Solr core是Solr-Home下的索引存儲地…可以建立多個,一個core有以下目錄
1. 需要建立這個core.找到solr-6.6.2\example\example-DIH\solr 下的db檔案夾…将其複制到solr-home中.并起名叫mycore(随便起) 注意mycore/conf中的schema-manaed檔案.此檔案是配置solr的索引資料. (這個一會會做詳細說明)
2.找到mycore/conf中的solrconfig.xml檔案…找到此處
<requestHandler name="/dataimport" class="solr.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>
其實隻要這裡配置的檔案名稱和mycore/conf中的data-config.xml名字一樣就行..
把該檔案名稱配置到這裡即可. .該檔案是solr索引庫中增量的配置地方---也就是資料庫導入索引庫需要配置的地方
到此處基本上就core就建立好了.
接下來需要對mycore/conf中的幾個檔案做詳細講解,包括各自的作用.
圖中标記的前2個檔案十分重要,下面那個改動一點點, 在Solr5.5版本之前,managed-schema 叫schema.xml…5.5版本之後才改成managed-schema .而網上大部分文檔還是5.5以前的,是以會是schema.xml…接下來就解開managed-schema這個檔案的神秘面紗.
之前提了這裡是索引庫中對字段的設定,是以就顯得十分重要…他的内部内容很多,但是無需關心那些,隻要關注3個标簽 .
- fieldType(自帶的分詞類型.有很多分詞器,但是沒有很好支援中文的分詞器.).
- field:這裡是配置單個字段的地方,着重配置這裡
- copyField:這裡是配置字段的域…也很重要.
上截圖
這裡面有個地方,對于searchkey這個多值域,有了更加靈活的方法,後期直接在solr用戶端query中進行查詢語句比對,無需像我這樣進行固定式配置,麻煩而且不靈活…講完這個,後面就講多值多域比對…
介紹上面截圖中field标簽中的屬性
field | 介紹上面截圖中field标簽中的屬性 |
---|---|
name | 和資料庫中的字段對應起别名,導入進索引庫以後對應的字段名就是這裡配置的如goods_name 對應這裡可以起别名goodsName |
type | 這裡配置字段的類型,要和資料庫中的字段類型一緻,選取的時候,其實上方配置都有定義類型,要注意和配置的一緻,比如string上方定義的時候是小寫,我們就别寫成大寫S了至于text_ik類型,後續會講解 |
multiValued | 是否是多值,比如 goodsColor 可以使紅,黃,藍…是以就看需不需要多值存儲了.存進索引庫都是以逗号分割… |
indexed | 是否需要索引,隻有需要當做域(比如根據goodsName:益生菌)來查詢,包括當做where條件來比對的字段有必要添加索引,其他可以不用建立索引. |
stored | 是否需要存儲到索引庫,一般需要傳回給使用者的都需要被存儲, |
copyField | 介紹上面截圖中copyField标簽中的屬性(可以了解成設定域) |
---|---|
source | 這裡就是配置field中name屬性的值,如果寫的上面field未定義的會報錯. |
dest | 這裡是設定域,需要一個多值的字段就行… |
多值多域比對
需求:相對多個字段同時進行檢索,隻要其中一個字段含有該關鍵字就比對出記錄,這裡就有 大寫的 OR 來組裝查詢語句
查詢既滿足A字段 又滿足B字段的記錄…則 查詢語句為 A AND(一定要大寫) B :輸入的關鍵字.
優化上面的多值多域比對…searchkey,可以無需像我那樣設定searchkey來指定goodsName和keywords
我們可以在solr用戶端這樣用,直接上截圖
四: 配置中文分詞器 (其實這一步可以放在第二步之前,放到這裡也沒關系)
由于solr中沒有對于中文有很好的智能分詞器,是以需要我們手動配置中文分詞器,我使用的是ik分詞器.
我使用Ik-Analyzer5.5 将解壓後的唯一一個jar包複制到solr-home\wscore\lib中.
然後将Ik-Analyzer下的3個檔案ext.dic 和stopword.dic還有IKAnalyzer.xml也一起複制到Tomcat8\webapps\solr\WEB-INF\classes目錄中
Ik-Analyzer5.5中的檔案 | 各個檔案的作用 |
---|---|
IKAnalyzer.xml | 這裡主要是指定ext.dic和stopword.dic…,隻要這3個檔案在同一目錄下就不用修改這裡 |
ext.dic | 是ik的自定義詞庫,我們可以把不當做關鍵字的詞,配置在這裡變成關鍵字…如:汝淉本身不是一個關鍵字,定義在這裡就是個關鍵字了. |
stopword.dic | 是ik的自定義停詞詞庫,意味着和ext.dic作用是相反的…比如女性是關鍵字,配到停詞庫中,分詞器就不會把女性當做關鍵字了 |
其中的ext.dic是ik的自定義詞庫,使用者可以進行自定義配置自己的詞庫…配置成功以後隻要再次啟動solr服務就生效了…
還有一步很重要:找到solr-home\core\conf中的managed-schema檔案…然後打開此檔案.加上這句話
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
<analyzer type="query" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
</fieldType>
上截圖
下面是對含有中文的字段配上ik,當然要有分詞使用想法的字段,不能凡是中文字段的都配(沒這個必要),
到此分詞器就配置成功了.我們可以到Solr的操作界面去看看…啟動我本地tomcat即可.然後通路http://localhost:10003/solr/index.html進行測試(我的tomcat端口号設定成10003)
在用戶端界面,切換到自己的core下面的分詞類目中.-----Analysis—界面.就會出現如下界面
看到這步,就說明分詞效果配置成功了…
拓展分詞詞庫–自定義詞庫
除了solr自身的分詞詞庫以外,有些詞ik并不識别,但是我們需要用到,我們需要建立自己需要的新的關鍵字 比如我輸入"褲子男士", ik會分成褲子,男士這2個關鍵字,除此以外我還想要關鍵字 "褲子男"這個關鍵字,這時候就得用到拓展四庫之自定義詞庫.
接下來我們就配置自定義詞庫…找到Tomcat8\webapps\solr\WEB-INF下的classes這個檔案…還記得這個檔案是在前幾步就建立好了…裡面放了ext.dic 和stopword.dic還有IKAnalyzer.xml還有一個日志檔案.總共4個檔案…之前提過ext.dic就是拓展詞庫的配置檔案…在這個檔案中專門存放自定義的關鍵字-----上截圖更直覺
然後重新開機tomcat就能生效… 同理停詞詞庫也是這樣配置的…把ik識别的關鍵字配到stopword.dic就不會在當成關鍵字了.
值得注意的是:ext.dic和stopword.dic…這2個檔案的編碼 必須是utf-8無bom編碼格式,否則不生效.
到此支援中文智能分詞的分詞器就配置成功了.
五:增量和全量資料導入
增量:其實solr就是一個NOSQL,隻是裡面存儲的都是被索引好的資料,查詢的時候就查詢索引庫中的資料就行了,但是問題來了,資料裡面的資料更新了,被之前導入到solr中的資料卻還是原來的–也就是solr和資料庫的資料不同步,基于這個需求,就衍生出了同步被更新的資料到solr中…
增量原理::需要資料庫來一個updateTime字段,凡是資料修改就更新該字段為目前時間…然後用這個字段和上一次增量執行時間作比較…如果updateTime>大于上一次索引更新時間,就說明該資料修改過了,需要重新增量導入…索引更新時間,一會截圖給出…
全量:相對于增量全量是把所有需要的資料全部重新導入到solr中
操作如下:
1. 找到solr-home\wscore\conf下的data-config.xml檔案…這裡就是配置資料庫增量的相關配置.
2. 在增量導入資料的時候需要一個jar包…這個jar包是資料庫的驅動…将此jar包上傳到Tomcat8\webapps\solr\WEB-INF\lib下…啟動tomcat就可以了,在solr用戶端就能測試了…
直接上傳本地處理好的schema-manaed這個檔案和data-config.xml檔案.
這是schema-manaed中相關字段的配置..
<field name="goodsId" type="long" multiValued="false" indexed="true" stored="true"/>
<field name="goodsName" type="text_ik" multiValued="false" indexed="true" stored="true"/>
<field name="goodsSn" type="text_ik" multiValued="false" indexed="false" stored="true"/>
<field name="keywords" type="text_ik" multiValued="false" indexed="true" stored="true"/>
<field name="isOnSale" type="int" multiValued="false" indexed="true" stored="true"/>
<field name="isDel" type="int" multiValued="false" indexed="true" stored="true"/>
<field name="isNoShow" type="int" multiValued="false" indexed="true" stored="true"/>
<field name="isSoldOut" type="int" multiValued="false" indexed="true" stored="true"/>
<field name="platType" type="int" multiValued="false" indexed="true" stored="true"/>
<field name="shopPrice" type="double" multiValued="false" indexed="true" stored="true"/>
<field name="marketPrice" type="double" multiValued="false" indexed="false" stored="true"/>
<field name="originalImg" type="string" multiValued="false" indexed="false" stored="true"/>
<field name="comments" type="long" multiValued="false" indexed="true" stored="true"/>
<field name="soldNum" type="long" multiValued="false" indexed="true" stored="true"/>
<field name="brandId" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="cateId" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="collectTimes" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="goodsColor" type="string" multiValued="false" indexed="false" stored="true"/>
<field name="goodsWeight" type="double" multiValued="false" indexed="false" stored="true"/>
<field name="iconImg" type="string" multiValued="false" indexed="false" stored="true"/>
<field name="isComm" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="isHot" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="isReal" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="memberPrice" type="double" multiValued="false" indexed="false" stored="true"/>
<field name="cashFenxiao" type="float" multiValued="false" indexed="false" stored="true"/>
<field name="cashAgentLevel5" type="float" multiValued="false" indexed="false" stored="true"/>
<field name="goodsThumb" type="string" multiValued="false" indexed="false" stored="true"/>
<field name="goodsImg" type="string" multiValued="false" indexed="false" stored="true"/>
<field name="preSale" type="int" multiValued="false" indexed="false" stored="true"/>
<field name="platCode" type="string" multiValued="false" indexed="true" stored="true"/>
<field name="searchkey" type="text_ik" multiValued="true" indexed="true" stored="false"/>
<field name="text" type="text_ik" multiValued="true" indexed="true" stored="false"/>
<copyField source="goodsId" dest="text"/>
<copyField source="goodsName" dest="searchkey"/>
<copyField source="goodsSn" dest="text"/>
<copyField source="keywords" dest="searchkey"/>
<copyField source="isOnSale" dest="text"/>
<copyField source="isSoldOut" dest="text"/>
<copyField source="isDel" dest="text"/>
<copyField source="isNoShow" dest="text"/>
<copyField source="platType" dest="text"/>
<copyField source="shopPrice" dest="text"/>
<copyField source="marketPrice" dest="text"/>
<copyField source="originalImg" dest="text"/>
<copyField source="comments" dest="text"/>
<copyField source="soldNum" dest="text"/>
<copyField source="brandId" dest="text"/>
<copyField source="cateId" dest="text"/>
<copyField source="collectTimes" dest="text"/>
<copyField source="goodsColor" dest="text"/>
<copyField source="goodsWeight" dest="text"/>
<copyField source="iconImg" dest="text"/>
<copyField source="isComm" dest="text"/>
<copyField source="isHot" dest="text"/>
<copyField source="isReal" dest="text"/>
<copyField source="memberPrice" dest="text"/>
<copyField source="cashFenxiao" dest="text"/>
<copyField source="cashAgentLevel5" dest="text"/>
<copyField source="goodsThumb" dest="text"/>
<copyField source="goodsImg" dest="text"/>
<copyField source="preSale" dest="text"/>
<copyField source="platCode" dest="text"/>
data-config.xml 索引導入檔案
data-config.xml檔案..索引導入檔案....
<dataConfig>
<dataSource
name="source"
type="JdbcDataSource"
driver="com.mysql.jdbc.Driver"
url="jdbc:mysql://*********/?zeroDateTimeBehavior=convertToNull"
user="root"
password="*************"/>
<document>
這裡是全量的entity,name是這個entity的名字可以自定義.pk就是主鍵,具體看這個模闆就能了解.
query是資料庫的sql,凡是被查出來的資料就會被導入進去..
<entity name="full_import_data" dataSource="source" pk="goods_id" query="select goods_id ,
goods_name,goods_sn ,keywords,convert(is_on_sale,UNSIGNED) as isOnSale,convert(is_delete,UNSIGNED)as isDel,
convert(is_no_show,UNSIGNED)as isNoShow,is_sold_out ,convert(plat_type,UNSIGNED) as platType,shop_price, market_price,
original_img ,comments,sold_num ,brand_id,cate_id,collect_times,goods_color,weight,icon_img,member_price,cash_fenxiao,
cash_agent_level5,goods_thumb,square_img,CONVERT (pre_sale, UNSIGNED) AS preSale,CONVERT (is_real, UNSIGNED) AS isReal,
CONVERT (is_hot, UNSIGNED) AS isHot,CONVERT (is_comm, UNSIGNED) AS isComm,plat_code
from wsmall_goods.gss_goods where is_delete=0 ">
這裡 資料庫中字段名字 schema-managed中的字段名
<field column="goods_id" name="goodsId" />
<field column="goods_sn" name="goodsSn" />
<field column="goods_name" name="goodsName" />
<field column="keywords" name="keywords" />
<field column="is_on_sale" name="isOnSale" />
<field column="is_delete" name="isDel" />
<field column="is_no_show" name="isNoShow" />
<field column="is_sold_out" name="isSoldOut" />
<field column="plat_type" name="platType" />
<field column="shop_price" name="shopPrice" />
<field column="market_price" name="marketPrice" />
<field column="original_img" name="originalImg" />
<field column="comments" name="comments" />
<field column="sold_num" name="soldNum" />
<field column="brand_id" name="brandId" />
<field column="cate_id" name="cateId" />
<field column="collect_times" name="collectTimes" />
<field column="goods_color" name="goodsColor" />
<field column="weight" name="goodsWeight" />
<field column="icon_img" name="iconImg" />
<field column="is_comm" name="isComm" />
<field column="is_hot" name="isHot" />
<field column="is_real" name="isReal" />
<field column="member_price" name="memberPrice" />
<field column="cash_fenxiao" name="cashFenxiao" />
<field column="cash_agent_level5" name="cashAgentLevel5" />
<field column="goods_thumb" name="goodsThumb" />
<field column="square_img" name="goodsImg" />
<field column="pre_sale" name="preSale" />
<field column="plat_code" name="platCode" />
</entity>
這裡是增量的entity...
deletedPkQuery:這裡是名額記那些被删除的記錄的主鍵id,因為這些記錄不需要導入到索引庫中..
deltaImportQuery:這個就是導入那些被修改未删除的記錄,,,未修改的資料不做操作,,還在索引庫中..
deltaQuery:這一步很重要:這裡是查詢出那些修改的資料,原理:需要資料庫來一個updateTime字段,
凡是資料修改就更新該字段為目前時間..然後用這個字段和上一次增量執行時間作比較..如果updateTime>大于
上一次索引更新時間,,就說明該資料修改過了,,需要重新增量導入...索引更新時間,一會截圖給出..
<entity name="delta_import_data" dataSource="source" pk="goods_id"
deletedPkQuery="select goods_id FROM wsmall_goods.gss_goods where is_delete=1"
deltaImportQuery="select goods_id ,
goods_name,goods_sn ,keywords,convert(is_on_sale,UNSIGNED) as isOnSale,convert(is_delete,UNSIGNED)as isDel,
convert(is_no_show,UNSIGNED)as isNoShow,is_sold_out ,convert(plat_type,UNSIGNED) as platType,shop_price, market_price,
original_img ,comments,sold_num ,brand_id,cate_id,collect_times,goods_color,weight,icon_img,member_price,cash_fenxiao,
cash_agent_level5,goods_thumb,square_img,CONVERT (pre_sale, UNSIGNED) AS preSale,CONVERT (is_real, UNSIGNED) AS isReal,
CONVERT (is_hot, UNSIGNED) AS isHot,CONVERT (is_comm, UNSIGNED) AS isComm,plat_code
from wsmall_goods.gss_goods where is_delete=0 and goods_id='${dataimporter.delta.goods_id}'"
deltaQuery="select goods_id FROM wsmall_goods.gss_goods where from_unixtime(update_time,'%Y-%m-%d %H:%i:%m') > '${dataimporter.last_index_time}'">
<field column="goods_id" name="goodsId" />
<field column="goods_sn" name="goodsSn" />
<field column="goods_name" name="goodsName" />
<field column="keywords" name="keywords" />
<field column="is_on_sale" name="isOnSale" />
<field column="is_delete" name="isDel" />
<field column="is_no_show" name="isNoShow" />
<field column="is_sold_out" name="isSoldOut" />
<field column="plat_type" name="platType" />
<field column="shop_price" name="shopPrice" />
<field column="market_price" name="marketPrice" />
<field column="original_img" name="originalImg" />
<field column="comments" name="comments" />
<field column="sold_num" name="soldNum" />
<field column="brand_id" name="brandId" />
<field column="cate_id" name="cateId" />
<field column="collect_times" name="collectTimes" />
<field column="goods_color" name="goodsColor" />
<field column="weight" name="goodsWeight" />
<field column="icon_img" name="iconImg" />
<field column="is_comm" name="isComm" />
<field column="is_hot" name="isHot" />
<field column="is_real" name="isReal" />
<field column="member_price" name="memberPrice" />
<field column="cash_fenxiao" name="cashFenxiao" />
<field column="cash_agent_level5" name="cashAgentLevel5" />
<field column="goods_thumb" name="goodsThumb" />
<field column="square_img" name="goodsImg" />
<field column="pre_sale" name="preSale" />
<field column="plat_code" name="platCode" />
</entity>
</document>
</dataConfig>
上一次索引時間記錄的檔案在solr-home/core/conf中的 dataimport.properties檔案…打開此檔案
配置好了,就需要我們執行了…看頁面截圖
全量導入用戶端操作
增量的導入操作,如圖
我增量和全量中遇到的大BUG
BUG1.solr對于資料庫類型為tinyint類型的字段,導入進索引庫中會變成Boolean類型的資料…這樣導緻資料失去原有數值表達的含義,建立的solr索引的也沒法使用…如何規避
解決方案
1.更改資料表,将tinyint類型轉化為int類型。(不推薦)
2.在DIH擷取MySQL資料集通過sql查詢時進行類型轉換,将查詢結果中tinyint類型轉換為int類型
MySQL中使用CONVERT(表達式,類型)函數進行類型轉換
select convert(is_on_sale,UNSIGNED) as isOnSale,convert(is_delete,UNSIGNED)as isDel,
convert(is_no_show,UNSIGNED)as isNoShow
from .....
BUG2.如果把增量和全量放在一個entity中,全量能執行,增量執行不了,sql其他都是正确的…
解決方案:把分成2個entity,增量一個 全量一個,執行的時候選擇指定的…
就像我上面傳的data-config.xml中的2個entity一樣,就可以執行了,隻要從一個entity分離出來就行了…具體原因不詳…
六:定時增量,定時全量
很多人希望定時增量,具體有2中方式…
第一種:solr自帶的,定時器,需要配置就行…因為我這個操作過,但都沒有成功,是以沒法貼出來…(不推薦),不推薦的原因不是自己沒有折騰出來,而是會增加solr的性能,一般用的多的人都會選擇代碼中的定時器來完成這個…
第二種:代碼中的定時器來完成…推薦…我寫了2中定時器…選擇自己合适的就行…
pom.xml中需要的依賴;
<!-- solr -->
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>6.6.2</version>
</dependency>
<!-- quartz 定時器-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>${quartz.version}</version>
</dependency>
上增量定時器代碼: 這個使用的是Spring的定時器
http://localhost:10005/solr/wscore2/dataimport?command=delta-import&entity=delta_import_evalu&clean=false&commit=true 這個位址就是增量的位址,隻要通路這個位址就能執行增量操作…是以定時器的任務就是去通路這個位址就行…
首先在web.xml中加上這句話…來監聽這個類…是以本應該那個位址(就是增量通路位址)配在配置檔案中的,但是,web.xml會首先加載,再去加載配置檔案,…是以就隻能放在這個被監聽的類中了…
<listener>
<listener-class>com.wsmall.solr.quartz.StartupListener4analysisReport</listener-class>
</listener>
上代碼:
package com.wsmall.solr.quartz;
import com.wsmall.solr.common.util.HttpClientUtil;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created by RuGuo on 2018/1/31.
*/
public class StartupListener4analysisReport implements ServletContextListener{
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(StartupListener4analysisReport.class);
//@Value("${solr.data.url}")
private static final String solrDataUrl = "http://localhost:10005/solr/wscore/dataimport";
//@Value("${solr.delta.param.url}")
private static final String solrDeltaUrl ="command=full-import&entity=full_import_data&clean=true&commit=true" ;
//evalu & media "http://localhost:10005/solr/wscore2/dataimport"
private static final String solrEvaluMediaDataUrl = "http://localhost:10005/solr/wscore2/dataimport";
private static final String solrDeltaEvaluUrl = "command=delta-import&entity=delta_import_evalu&clean=false&commit=true";
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
servletContextEvent.getServletContext().log("啟動線程池");
servletContextEvent.getServletContext().log("啟動定時器");
//執行goods增量導入
Runnable runnableDelta = new Runnable() {
public void run() {
// task to run goes here
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=simpleDateFormat.format(new Date());
logger.warn("solr定時增量執行循環**delta**"+ time + "**delta**");
String responseJson = HttpClientUtil.get(solrDataUrl,solrDeltaUrl);
}
};
ScheduledThreadPoolExecutor service = new ScheduledThreadPoolExecutor(5);
//ScheduledExecutorService service = Executors
//.newSingleThreadScheduledExecutor();
//5分鐘執行一次增量
service.scheduleAtFixedRate(runnableDelta, 0, 5, TimeUnit.MINUTES);
//service.scheduleAtFixedRate(runnableFull, 0, 7, TimeUnit.MINUTES);
//執行evalu增量導入
Runnable runnableDeltaEvalu = new Runnable() {
public void run() {
// task to run goes here
logger.warn("solr定時Evalu增量執行循環**Evalu**"+"**delta**");
String responseJson = HttpClientUtil.get(solrEvaluMediaDataUrl,solrDeltaEvaluUrl);
}
};
ScheduledThreadPoolExecutor serviceEvalu = new ScheduledThreadPoolExecutor(5);
serviceEvalu.scheduleAtFixedRate(runnableDeltaEvalu, 0, 7, TimeUnit.MINUTES);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
servletContextEvent.getServletContext().log("定時器銷毀");
}
}
全量導入代碼…全量采用的是quartz定時器 上代碼
package com.wsmall.solr.quartz;
import com.wsmall.solr.common.util.HttpClientUtil;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Created by RuGuo on 2018/1/25.
*/
public class QuartzFullImport {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(QuartzFullImport.class);
@Value("${solr.data.url}")
private String solrDataUrl;//同樣都是通路位址,,,這個配在配置檔案中,,友善管理
@Value("${solr.full.param.url}")
private String solrFullUrl;
配置檔案中的:
solr.data.url=http://localhost:10005/solr/wscore/dataimport
solr.full.param.url=command=full-import&entity=full_import_data&clean=true&commit=true
/**
* 定時全量導入
*/
public void work()
{
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time =simpleDateFormat.format(new Date());
logger.warn("solr定時全量執行循環//"+ time+"/");
String responseJson = HttpClientUtil.get(solrDataUrl,solrFullUrl);
}
}
quartz的配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd">
<!-- 線程執行器配置,用于任務注冊 -->
<bean id="executor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="queueCapacity" value="500" />
</bean>
<!-- 任務對象 -->
<bean name="quartzFullImport" class="com.wsmall.solr.quartz.QuartzFullImport" />
<bean name="quartzFullEvaluImport" class="com.wsmall.solr.quartz.QuartzFullEvaluImport" />
<!-- 定時任務 -->
<!-- ============= 排程業務============= -->
<bean id="quartzFullImportTask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="quartzFullImport"></property>
<!-- 排程的方法名 -->
<property name="targetMethod" value="work"></property>
<!-- 如果前一個任務還沒有結束第二個任務不會啟動 false -->
<property name="concurrent" value="true"></property>
</bean>
<!-- ============= 排程業務============= -->
<bean id="quartzFullEvaluImportTask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="quartzFullEvaluImport"></property>
<!-- 排程的方法名 -->
<property name="targetMethod" value="importFullEvalu"></property>
<!-- 如果前一個任務還沒有結束第二個任務不會啟動 false -->
<property name="concurrent" value="true"></property>
</bean>
<!--//-->
<!-- 定時任務觸發器 -->
<bean id="quartzFullImportTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="quartzFullImportTask"/>
<!-- 0 30 14 ? * TUE表示每個星期一淩晨3點 MON,TUE,WED,THU,FRI,SAT,SUN-->
<property name="cronExpression" value="0 1 2 ? * *"></property>
</bean>
<bean id="quartzFullEvaluImportTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="quartzFullEvaluImportTask"/>
<!-- 0 30 10 ? * * 每天早上10點30分觸發-->
<property name="cronExpression" value="0 30 10 ? * *"></property>
</bean>
<!-- ============= 排程工廠 ============= -->
<bean name="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref local="quartzFullImportTrigger" />
<ref local="quartzFullEvaluImportTrigger" />
</list>
</property>
</bean>
</beans>
HttpClientUtil 工具類
package com.wsmall.solr.common.util;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
/**
* Created by pan on 12/01/2017.
*/
public class HttpClientUtil {
private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);
public static String postWithJson(String url, String jsonValues) {
HttpClient httpClient = HttpClientBuilder.create().build();
try {
HttpPost request = new HttpPost(url);
StringEntity params = new StringEntity(jsonValues);
request.addHeader("content-type", "application/json");
request.setEntity(params);
HttpResponse response = httpClient.execute(request);
return parseResponse(response);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/**
* // * 向指定url發送get方法的請求 // * // * @param url 發送請求的url // * @param param 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。
* // * @return result 所代表遠端資源的響應結果 //
*/
public static String get(String url, String param) {
// 傳回結果
String result = "";
BufferedReader in;
in = null;
try {
// url
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打開和URL之間的連接配接
URLConnection connection = realUrl.openConnection();
// 設定通用的請求屬性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立實際的連接配接
connection.connect();
// 擷取所有響應頭字段
// Map<String, List<String>> map = connection.getHeaderFields();
// 周遊所有的響應頭字段
/*
* for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); }
*/
// 定義 BufferedReader輸入流來讀取URL的響應
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
// 使用finally塊來關閉輸入流
finally {
close(in);
}
// 如果外部接口傳回為空,傳回其他錯誤原因
if (StringUtils.isBlank(result)) {
result = "{\"result\":\"other error\"}";
}
return result;
}
/**
* 将伺服器傳回的資料包裝成為String
*
* @param response
* @return
* @throws IOException
*/
private static String parseResponse(HttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
logger.error("Method failed:" + response.getStatusLine());
}
// Read the response body
return EntityUtils.toString(response.getEntity());
}
/**
* @param closeable
*/
private static void close(Closeable... closeable) {
for (Closeable c : closeable) {
try {
if (c != null) {
c.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
solr查詢接口…上代碼:
model ------------GoodsVO.java 隻截取一部分,有點長…注意value,一會提他的作用
package com.wsmall.solr.pojo.goods.vo;
import org.apache.solr.client.solrj.beans.Field;
import java.io.Serializable;
import java.util.Date;
/**
* Created by RuGuo on 2017/12/7.
*/
public class GoodsVo implements Serializable{
//@Field("id")
//private String id; //索引主鍵
@Field("goodsId")
private Long goodsId; //商品主鍵
@Field("goodsSn")
public Long getGoodsId() {
return goodsId;
}
public void setGoodsId(Long goodsId) {
this.goodsId = goodsId;
}
public String getGoodsSn() {
return goodsSn;
}
public void setGoodsSn(String goodsSn) {
this.goodsSn = goodsSn;
}
}
query的代碼
solr.url=http://localhost:10005/solr/wscore
@Override
public PageRecords<List<GoodsVo>> query(GoodsQueryParam goodsParam) {
PageRecords<List<GoodsVo>> pageRecords = PageRecords.newByExisted(goodsParam);
try {
SolrClient solr = new HttpSolrClient.Builder(solrUrl).build();
//HttpSolrClient httpSolrClient = SolrUtils.connect();
SolrQuery query = new SolrQuery();
//查詢條件
if (StringUtils.isBlank(goodsParam.getSearchkey())) {
query.setQuery("*:*");
} else {
query.setQuery(goodsParam.getSearchkey());
}
//設定預設搜尋域
//query.set("df", "keyWords");
if (null != goodsParam.getIsOnSale()){
query.addFilterQuery("isOnSale:"+goodsParam.getIsOnSale());
}
if(null != goodsParam.getIsNoShow()){
query.addFilterQuery("isNoShow:"+goodsParam.getIsNoShow());
}
if(null != goodsParam.getPlatType()){
query.addFilterQuery("platType:"+goodsParam.getPlatType());
}
if(null != goodsParam.getIsSoldOut()){
query.addFilterQuery("isSoldOut:"+goodsParam.getIsSoldOut());
}
if(null != goodsParam.getIsDel()){
query.addFilterQuery("isDel:"+goodsParam.getIsDel());
}else{
query.addFilterQuery("isDel:0");
}
//排序
if(null != goodsParam.getOrderBy()){
if("shopPrice".equals(goodsParam.getOrderBy())){
query.set("sort", "shopPrice "+goodsParam.getSortCat());
}
if("soldNum".equals(goodsParam.getOrderBy())){
query.set("sort", "soldNum "+goodsParam.getSortCat());
}
if("comments".equals(goodsParam.getOrderBy())){
query.set("sort", "comments "+goodsParam.getSortCat());
}
}
//分頁開始頁數
query.setStart(goodsParam.getBegin());
//設定傳回記錄數,預設為10條
query.setRows(goodsParam.getRows());
QueryResponse response = solr.query(query);
SolrDocumentList list = response.getResults();
Long recordCount = list.getNumFound();
//Long recordSlipt = 0L;
List<GoodsVo> goodsVoList = null;
if(recordCount!=0){
//還記得model中的字段是為什麼有value注解嗎?
//作用就展現在這裡了,,轉換需要到這個注解,,
goodsVoList = response.getBeans(GoodsVo.class);
pageRecords.setRecords(goodsVoList);
pageRecords.setTotalRows(Integer.parseInt(recordCount.toString()));
return pageRecords;
}
} catch (SolrServerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
分頁工具
package com.wsmall.solr.common.db;
import com.wsmall.solr.common.Page;
/**
* Created by pan on 15/11/2016.
*/
public class PageRecords<T> extends Page {
private T records; // 結果集
public PageRecords() {
}
// public PageRecords(int currentPage, int rows) {
// super(currentPage, rows);
// }
public static <A> PageRecords<A> newByExisted(Page formPage) {
PageRecords<A> page = new PageRecords();
page.setCurrentPage(formPage.getCurrentPage());
page.setRows(formPage.getRows());
page.setTotalRows(formPage.getTotalRows());
page.setDisabledCountPage(formPage.isDisabledCountPage());
page.setDisabledPage(formPage.isDisabledPage());
return page;
}
public PageRecords(int currentPage, int rows, int totalRows) {
super(currentPage, rows, totalRows);
}
public T getRecords() {
return records;
}
public void setRecords(T records) {
this.records = records;
}
/**
* 擷取統計條數,如若沒有禁用統計功能
* @param queryable
*/
public void queryByPage(PageQueryable<T> queryable) {
// 若禁用分頁,或者分頁查詢就跳過count方法
if (!isDisabledPage() && !isDisabledCountPage()) {
// 查詢并設定分頁
this.setTotalRows(queryable.count());
// 無資料就無須繼續往下查詢query方法
if (this.getRows() < 1) {
return;
}
}
T t = queryable.query();
setRecords(t);
}
}
page類
package com.wsmall.solr.common;
/**
* Created by zgj on 2015-03-03.
*/
public class Page {
/**
* 查詢記錄從第幾條開始
*/
private int begin = 0;
/**
* 目前頁碼
*/
private int currentPage = 1;
/**
* 每頁記錄數
*/
private int rows = 10;
/**
* 總頁數
*/
private int totalPages = 0;
/**
* 總記錄數
*/
private int totalRows = 0;
private boolean disabledCountPage = Boolean.TRUE;
private boolean disabledPage;
public Page() {
}
public Page(int currentPage, int rows) {
this.currentPage = currentPage;
this.rows = rows;
this.begin = (currentPage - 1) * rows;
}
public Page(int currentPage, int rows, int totalRows) {
this(currentPage, rows);
setTotalRows(totalRows);
}
public int getCurrentPage() {
return currentPage;
}
public void setCurrentPage(Integer currentPage) {
if (currentPage != null) {
this.currentPage = Math.max(currentPage, 1);
this.begin = (this.currentPage - 1) * rows;
}
}
public int getRows() {
return rows;
}
public void setRows(Integer rows) {
if (rows != null) {
this.rows = rows;
}
setCurrentPage(this.currentPage);
}
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(Integer totalPages) {
if (totalPages != null) {
this.totalPages = totalPages;
if (rows > 0 && totalPages > 0) {
this.totalRows = rows * totalPages;
}
}
}
public int getTotalRows() {
return totalRows;
}
public void setTotalRows(Integer totalRows) {
if (totalRows != null) {
this.totalRows = totalRows;
if (rows > 0 && totalRows > 0) {
this.totalPages = (int) (Math.ceil((double) totalRows / rows));
}
}
}
public int getBegin() {
return begin;
}
public void setBegin(Integer begin) {
if (begin != null) {
this.begin = begin;
}
}
public boolean isDisabledPage() {
return disabledPage;
}
public void setDisabledPage(boolean disabledPage) {
this.disabledPage = disabledPage;
}
public boolean isDisabledCountPage() {
return disabledCountPage;
}
public void setDisabledCountPage(boolean disabledCountPage) {
this.disabledCountPage = disabledCountPage;
}
}
如果轉載,請說明出處.謝謝.
到此,我能接觸的業務就結束了…都是個人自己寫的,肯定會有一些不當的地方,還希望海涵,當然指出來多交流…