ElasticSearch7.x系列四:實戰


前言

前面的三篇系列了解了安裝和一些基本使用,這一章節呢,我來個實戰.

首先不管是在公司使用還是個人使用,面臨的最初的問題是:

  1. ElasticSearch系列包括ES,ES-head,Kibana,分詞器等怎么安裝?
  2. 我的數據庫里面的數據表怎么同步到ES里面?而且是增量更新?
  3. 我客戶端怎么調用?怎么高亮顯示?怎么全文檢索,條件檢索?

我抱着這3個問題去學習ElasticSearch的時候發現,網上沒有很好的文章,包括現在2020年5月50日,在網上搜一下ElasticSearch的文章,絕大部分都是官方文檔的翻譯,同步數據寫的都是ES操作的IndexDocument方法,我就奇怪了,你們數據表里幾百萬的數據就用ES的插入方法去插入?

客戶端調用呢我使用的是.net,在ES7.x版本之后type被廢除了,.net客戶端使用的是Nest,同樣的,現在在網上搜,也沒有很好的文章,大都還是官方文檔的簡單翻譯,連個高亮,多條件搜索都沒.

So,我寫了這一篇文章,也就是系列四實戰篇,跟着我做,你可以得到

  1. 數據表增量的同步到ES,包括增加,更新,但是不包括刪除
  2. .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,所以目前兩種方式都可以

  1. 3張表,建立3個index索引
  2. 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那一篇,講的很詳細,看不懂可以去看看

注意一下

  1. jdbc_driver_library換成你們自己的驅動路徑
  2. jdbc_connection_string換成你們自己的數據庫連接字符串,下面的賬號密碼同理
  3. 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種方法

  1. 數據庫使用偽刪除,IsDelete字段,0變1,然后定期清理數據庫和ES中IsDelete為1的數據
  2. 在代碼中刪除數據庫的時候直接調用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語法需要了解學習,不過剩下的看文檔也差不多了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM