ElasticSearch7.x系列四:实战
前言
前面的三篇系列了解了安装和一些基本使用,这一章节呢,我来个实战.
首先不管是在公司使用还是个人使用,面临的最初的问题是:
- ElasticSearch系列包括ES,ES-head,Kibana,分词器等怎么安装?
- 我的数据库里面的数据表怎么同步到ES里面?而且是增量更新?
- 我客户端怎么调用?怎么高亮显示?怎么全文检索,条件检索?
2022年更新
我并没有使用Logstash来同步ES数据,我采用的方法是,先把数据库的数据批量的插入ES,然后后面的增删改查都加一个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语法需要了解学习,不过剩下的看文档也差不多了