前言
前面的三篇系列了解了安裝和一些基本使用,這一章節呢,我來個實戰.
首先不管是在公司使用還是個人使用,面臨的最初的問題是:
- ElasticSearch系列包括ES,ES-head,Kibana,分詞器等怎么安裝?
- 我的數據庫里面的數據表怎么同步到ES里面?而且是增量更新?
- 我客戶端怎么調用?怎么高亮顯示?怎么全文檢索,條件檢索?
我抱着這3個問題去學習ElasticSearch的時候發現,網上沒有很好的文章,包括現在2020年5月50日,在網上搜一下ElasticSearch的文章,絕大部分都是官方文檔的翻譯,同步數據寫的都是ES操作的IndexDocument方法,我就奇怪了,你們數據表里幾百萬的數據就用ES的插入方法去插入?
客戶端調用呢我使用的是.net,在ES7.x版本之后type被廢除了,.net客戶端使用的是Nest,同樣的,現在在網上搜,也沒有很好的文章,大都還是官方文檔的簡單翻譯,連個高亮,多條件搜索都沒.
So,我寫了這一篇文章,也就是系列四實戰篇,跟着我做,你可以得到
- 數據表增量的同步到ES,包括增加,更新,但是不包括刪除
- .net里的Nest客戶端高亮搜索,包括全文檢索,單字段搜索和多條件搜索
前言說完,我們開始吧.
ps:lucca我知道你在看🐷
ES系列安裝
這個看前三個系列,你需要安裝ES,ES-head,分詞器,Kibana,Logstash
其中ES-head,Kibana只是可視化的工具,不安裝也可以,但是建議至少安裝一個ES-head
去看我前三篇安裝好,我這里使用的是Windows版本的,linux下Docker安裝網上搜即可
數據同步
我有好幾個數據表,大概幾百萬的數據,使用ES的插入方法顯然不現實,所以我們這里使用Logstash進行數據的同步
比如我現在有3個表,分別是新聞表,視頻表,文章表,我的站內搜索也針對這3張表進行,由於ES7.x版本廢棄了type,所以目前兩種方式都可以
- 3張表,建立3個index索引
- 3張表,建立1個index索引,但是加一個estype字段進行區分
這兩種方式都是可以的,但是我使用的是3張表,我就建立3個index索引
明確了這個概念之后,我們開始寫Logstash的配置文件,首先你要確定你的數據庫是SQLserver還是Mysql,要去下載對應的JDBC驅動,而且Logstash要安裝JDK
我這里使用的是SQLserver,所以直接搜索 : Microsoft SQL Server JDBC
,然后下載驅動即可
我習慣把驅動放在Logstash的bin目錄下,在bin目錄下新建一個文件夾叫: jdbcconfig
然后驅動放進去,開始寫配置文件,隨便起個名稱,比如我的jdbc.config
input {
jdbc {
jdbc_driver_library => "D:\Vae\ElasticSearch\logstash-7.6.2\logstash-7.6.2\bin\jdbcconfig\mssql-jdbc-8.2.2.jre8.jar"
jdbc_driver_class => "com.microsoft.sqlserver.jdbc.SQLServerDriver"
jdbc_connection_string => "jdbc:sqlserver://192.168.100.100:1433;DatabaseName=VaeDB;"
jdbc_user => "sa"
jdbc_password => "666666"
schedule => "* * * * *"
statement => "select NewsID as Id,Title as title,CreateDate as createDate,Content as content,CONVERT (VARCHAR (30),UpdateDate,25) AS UpdateDate from News where UpdateDate > :sql_last_value"
use_column_value => true
tracking_column => "UpdateDate"
tracking_column_type => "timestamp"
type => "News"
}
jdbc {
jdbc_driver_library => "D:\Vae\ElasticSearch\logstash-7.6.2\logstash-7.6.2\bin\jdbcconfig\mssql-jdbc-8.2.2.jre8.jar"
jdbc_driver_class => "com.microsoft.sqlserver.jdbc.SQLServerDriver"
jdbc_connection_string => "jdbc:sqlserver://192.168.100.100:1433;DatabaseName=VaeDB;"
jdbc_user => "sa"
jdbc_password => "666666"
schedule => "* * * * *"
statement => "select ArticleId as Id,Title as title,CreateDate as createDate,Content as content,CONVERT (VARCHAR (30),UpdateDate,25) AS UpdateDate from Article where UpdateDate > :sql_last_value"
use_column_value => true
tracking_column => "UpdateDate"
tracking_column_type => "timestamp"
type => "Article"
}
jdbc {
jdbc_driver_library => "D:\Vae\ElasticSearch\logstash-7.6.2\logstash-7.6.2\bin\jdbcconfig\mssql-jdbc-8.2.2.jre8.jar"
jdbc_driver_class => "com.microsoft.sqlserver.jdbc.SQLServerDriver"
jdbc_connection_string => "jdbc:sqlserver://192.168.100.100:1433;DatabaseName=VaeDB;"
jdbc_user => "sa"
jdbc_password => "666666"
schedule => "* * * * *"
statement => "select VideoId as Id,Title as title,CreateDate as createDate,Content as content,CONVERT (VARCHAR (30),UpdateDate,25) AS UpdateDate from Video where UpdateDate > :sql_last_value"
use_column_value => true
tracking_column => "UpdateDate"
tracking_column_type => "timestamp"
type => "Video"
}
}
filter {
mutate {
add_field => {
"[@metadata][NewsID]" => "%{Id}"
}
add_field => {
"[@metadata][ArticleId]" => "%{Id}"
}
add_field => {
"[@metadata][VideoId]" => "%{Id}"
}
}
}
output {
if [type] == "News"{
elasticsearch {
hosts => "192.168.100.100:9200"
index => "news"
action => "index"
document_id => "%{[@metadata][NewsID]}"
}
}
if [type] == "Article"{
elasticsearch {
hosts => "192.168.100.100:9200"
index => "article"
action => "index"
document_id => "%{[@metadata][ArticleId]}"
}
}
if [type] == "Video"{
elasticsearch {
hosts => "192.168.100.100:9200"
index => "video"
action => "index"
document_id => "%{[@metadata][VideoId]}"
}
}
}
關於這個配置文件,我的系列三Logstash那一篇,講的很詳細,看不懂可以去看看
注意一下
- jdbc_driver_library換成你們自己的驅動路徑
- jdbc_connection_string換成你們自己的數據庫連接字符串,下面的賬號密碼同理
- output里面的ElasticSearch hosts換成你們自己的ES地址
ok配置文件寫完,我們之間執行一下,在Logstash的bin目錄打開windows的power shell輸入
logstash -f jdbcconfig/jdbc.conf
稍等一會,你打開ES-head或者Kibana就可以看到3個表的數據全部同步到ES里對應的3個Index了
由於我用的UpdateDate更新時間作為更新依據字段,所以新的增加和更新操作都會增量的更新,更新頻率自己設置,不懂的看系列三
增加和更新Logstash會幫助我們,那么刪除呢?
官方給了2種方法
- 數據庫使用偽刪除,IsDelete字段,0變1,然后定期清理數據庫和ES中IsDelete為1的數據
- 在代碼中刪除數據庫的時候直接調用ES的刪除方法,刪了ES里的數據
這兩種方式看吧,都可以,如果表少的話使用2不錯
.net代碼怎么搜索ES
跟着我做到這一步,數據已經有了,那么代碼咋寫?在.net里面的NuGet搜索Nest,安裝
然后順便說一個,我多Index搜索的時候會面臨一個問題
怎么接受多Index數據?
因為我的新聞表,視頻表的字段都不一樣,我接受的時候,Nest這玩意只能寫一個Model接受
我想到了兩種方法,一種是寫泛型,如下
client.Search<T>(s => s
.Index(indexName)
.Query(q => q
.Match(m => m
.Field(f => f.Title)
.Query(keyword))
)
但是Nest這玩意很惡心你知道嗎?我寫泛型當然可以,但是我下面的Title檢索就報錯了
.Field(f => f.Title)
文檔好像啥都沒寫,網上搜的文章好像有這種寫法
.Field("title")
我一看這也行啊,這樣我的泛型就可以用了,但是報錯.我沒試出來,你們可以試試
所以我采取了另外一種辦法,使用一個Model接收,也就是我定義的ViewModel AllInformationViewModel
數據庫表字段我全部as了一次,3張表查出來的結果都是只有Id,Title,Content,createDate這幾個字段
反正我ES查詢的結果也就展示這些內容,干脆全部名稱一致,我也方便.
單Index和多Index
我只想搜視頻表的內容,那就單Index,我想搜視頻,新聞,文章3個表里面的內容,那就多Index
string[] indexName = new string[] { "article", "news", "video" };
client.Search<AllInformationViewModel>(s => s
.Index(indexName)
很完美,indexName是單索引還是多索引自己傳值
單字段搜索和全文檢索
我想搜標題,那就是標題高亮,我想搜全文,包括標題和正文描述,那就兩個都高亮
#只搜標題
client.Search<AllInformationViewModel>(s => s
.Index(indexName)
.Query(q => q
.Match(m => m
.Field(f => f.Title)
.Query(keyword))
)
#全文檢索
client.Search<AllInformationViewModel>(s => s
.Index(indexName)
.Query(q => q
.QueryString(qs => qs
.Query(keyword).DefaultOperator(Operator.And))
多條件,時間范圍+分頁+高亮
我懶得寫了,貼出代碼吧,下面這個是全文檢索的時間范圍+分頁+全文高亮
return client.Search<AllInformationViewModel>(s => s
.Index(indexName)
.From(pageInfo.PageIndex)
.Size(pageInfo.PageSize)
.Query(q => q
.QueryString(qs => qs
.Query(keyword).DefaultOperator(Operator.And))
&& q
.DateRange(d => d
.Field(f => f.CreateDate)
.GreaterThanOrEquals(startTime)
.LessThan(endTime)
)
)
.Highlight(h => h
.PreTags("<em>")
.PostTags("</em>")
.Fields(
fs => fs
.Field(p => p.Title),
fs => fs
.Field(p => p.Content)
)));
還有一個有意思的,就是ES查出來的高亮在Hit的Highlight里面,你得手動的去賦值
if (search.Hits?.Count() > 0)
{
foreach (var hit in search.Hits)
{
var allInformationViewModel = new AllInformationViewModel
{
Id = int.Parse(hit.Id),
KeyName = hit.Source.KeyName,
Title = hit.Source.Title,
Content = hit.Source.Content,
Score = hit.Score,
Etype = hit.Source.Etype,
Picture = hit.Source.Picture,
CreateDate = hit.Source.CreateDate
};
foreach (var highlightField in hit.Highlight)
{
if (highlightField.Key == "title")
{
foreach (var highlight in highlightField.Value)
{
allInformationViewModel.Title = highlight;
}
}
else if (highlightField.Key == "content")
{
allInformationViewModel.Content = string.Empty;
short num = 0;
foreach (var highlight in highlightField.Value)
{
allInformationViewModel.Content += DataValidator.CleanHTMLExceptem(highlight) + "...";
num += 1;
if (num > 3)
{
break;
}
}
}
}
allInformationViewModels.Add(allInformationViewModel);
}
}
上面的代碼很簡單,直接取出Highlight里面的高亮,判斷是title的高亮就賦值給Title,判斷是content的高亮就賦值給Content字段,但是Content正文可能有好幾個值,我就取3個展示足夠了,中間用...分隔一下
由於正文Content大部分情況含有HTML標簽,所以需要去除一下HTML標簽,但是不去除em標簽,因為em是我們的高亮標簽
使用QueryContainerDescriptor查詢
字段查詢和全文檢索不能寫兩次吧,使用QueryContainerDescriptor
Func<SortDescriptor<AllInformationViewModel>, IPromise<IList<ISort>>> sortDesc = sd =>
{
switch (sort)
{
case Sort.Score:
sd.Descending(SortSpecialField.Score);
break;
case Sort.CreateDate:
sd.Descending(d => d.CreateDate);
break;
default:
sd.Descending(SortSpecialField.Score);
break;
}
return sd;
};
var mustQuerys = new List<Func<QueryContainerDescriptor<AllInformationViewModel>, QueryContainer>>();
switch (allText)
{
case AllText.TitleSearch:
mustQuerys.Add(mt => mt
.Match(qs => qs.Field(f => f.Title).Query(keyword))
&& mt
.DateRange(d => d
.Field(f => f.CreateDate)
.GreaterThanOrEquals(startTime)
.LessThan(endTime)
));
break;
case AllText.AllTextSearch:
mustQuerys.Add(mt => mt
.QueryString(qs => qs.Query(keyword).DefaultOperator(Operator.And))
&& mt
.DateRange(d => d
.Field(f => f.CreateDate)
.GreaterThanOrEquals(startTime)
.LessThan(endTime)
));
break;
default:
mustQuerys.Add(mt => mt.Match(qs => qs.Field(f => f.Title).Query(keyword)));
break;
}
var search = client.Search<AllInformationViewModel>(s => s
.Index(indexName)
.From((pageInfo.PageIndex - 1) * pageInfo.PageSize)
.Size(pageInfo.PageSize)
.Query(q => q.Bool(b => b.Must(mustQuerys)))
.Highlight(h => h
.PreTags("<em>")
.PostTags("</em>")
.Fields(
fs => fs
.Field(p => p.Title),
fs => fs
.Field(p => p.Content)
))
.Sort(sortDesc)
);
完結
這篇實戰,跟着做下來,你可以得到一個最基礎的功能了,最基礎的數據+搜索是可以了,前端頁面很簡單,我沒寫,直接一個ul li完事,然后方法參數啥的自己定義,ES封裝一下搞個Helper類,搞個靜態實例,基本的方法封裝一下完事.
但是還有ES的安全,以及其他Nest語法需要了解學習,不過剩下的看文檔也差不多了