ElasticSearch7.x系列四:实战

前言

前面的三篇系列了解了安装和一些基本使用,这一章节呢,我来个实战.

首先不管是在公司使用还是个人使用,面临的最初的问题是:

  1. ElasticSearch系列包括ES,ES-head,Kibana,分词器等怎么安装?
  2. 我的数据库里面的数据表怎么同步到ES里面?而且是增量更新?
  3. 我客户端怎么调用?怎么高亮显示?怎么全文检索,条件检索?

2022年更新
我并没有使用Logstash来同步ES数据,我采用的方法是,先把数据库的数据批量的插入ES,然后后面的增删改查都加一个ES的操作,去增删改查数据


我抱着这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语法需要了解学习,不过剩下的看文档也差不多了

posted @ 2020-05-30 16:15  蜀云泉  阅读(2564)  评论(1编辑  收藏  举报