0 引言
某些业务场景下我们需要使用特殊符号来进行查询,但是es的默认分词器以及ik分词器等大多数分词器都会将特殊符号过滤掉,导致后续无法通过特殊符号查询到数据。
那么我们如何来解决这个问题呢,下面列举出几种处理方案
1 问题复现
1、设置mapping
PUT test_special
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
2、插入带有特殊符号的数据
PUT test_special/_doc/1
{"name":"55555@#!sina.com"}
3、通过‘@’去查询原数据
GET test_special/_search
{
"query": {
"match": {
"name": "@#!"
}
}
}
4、结果为空
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
}
2 解决方案
2.1 使用keyword查询
首先从业务层考虑,是否需要单个的特殊符号查询,还是说是带上特殊符号的查询词搜索,所以先从最简单的用keyword+term查询试试看能够满足业务需求
GET test_special/_search
{
"query": {
"term": {
"name.keyword": "55555@#!sina.com"
}
}
}
2.2 mapping映射,防止过滤
这里的特殊字符的别名用两个点隔开,并且加上了xxx
前缀,点的作用是为了让其被单独分词,也可以替换成逗号,前缀的目的是为了让特殊字符的别名唯一,后续插入的数据不会和这个重复。xxx
可以是你公司的名称或者其他含义的前缀。
PUT test_special
{
"settings": {
"analysis": {
"analyzer": {
"special_analyzer": {
"tokenizer": "standard",
"char_filter": [
"special_mapping"
]
}
},
"char_filter": {
"special_mapping":{
"type": "mapping",
"mappings": [
"@=>.xxxAt.",
"#=>.xxxJin.",
"!=>.xxxSigh.",
"*=>.xxxXing."
]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "special_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
如上方法的原理是将特殊字符转换为能识别的普通字符组合,只是这个字符组合具备唯一性,我们单独查看下分词结果就能一目了然
GET test_special/_analyze
{
"analyzer": "special_analyzer",
"text": [
"55555@#*sina.com"
]
}
分词结果
"tokens" : [
{
"token" : "55555",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<NUM>",
"position" : 0
},
{
"token" : "xxxAt",
"start_offset" : 5,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "xxxJin",
"start_offset" : 6,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "xxxXing.sina.com",
"start_offset" : 7,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 3
}
]
查询测试
GET test_special/_search
{
"query": {
"match": {
"name": "@#*"
}
}
}
结果中可以将数据查询出来,查询词在查询时因为使用的match查询,因此也会被分词,并且调用的分词器就是查询字段的分词器,因此特殊符号就被替换且分词为了:xxxAt
,xxxJin
,xxxXing
,通过这些分词是可以查询到结果的
"hits" : [
{
"_index" : "test_special",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.5098253,
"_source" : {
"name" : "55555@#!sina.com"
}
}
]
后续如果有新的特殊字符如何更新呢?
官方文档中有介绍,mapping char filter中除了直接写mappings外,还可以通过mappings_path,将mappings内容写到文本文件中,然后在mappings_path中配置这个文件的路径,就可以直接从这个文件中读取,下次更新时就更新文件,然后重启集群即可。这里需要注意的是该文本必须是UTF-8编码的,同时mappings_path路径要么是绝对路径要么是基于es config路径的相对路径
2.2 ngram分词器,设置min_gram为1
es中还提供了一个ngram分词器,可以将字段按照指定的长度进行分词
我们先通过一个实例来体会它的作用,比如字符串“12345”,设置了如下的分词器,那么分词结果就是1,12,2,23,3,34,4,45,5
也就是说它会将其按照最小长度为1,最大长度为2的范围进行分词。同时如何字符串中包含特殊字符或者空格也会保留,将其按照一个字符处理
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 1,
"max_gram": 2
}
}
因此,我们创建索引,这里的min_gram必须设置为1,max_gram可以根据业务情况设置
PUT test_special
{
"settings": {
"analysis": {
"analyzer": {
"special_analyzer": {
"tokenizer": "ngram_tokenizer"
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 1,
"max_gram": 2
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "special_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
插入数据,并查询测试
PUT test_special/_doc/1
{"name":"55555@#!sina.com"}
GET test_special/_search
{
"query": {
"match": {
"name": "@#!"
}
}
}
结果
"hits" : [
{
"_index" : "test_special",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.4384104,
"_source" : {
"name" : "55555@#!sina.com"
}
}
]
优缺点:
这种方式分隔的粒度很小,能够很准确的查询到我们要的数据,但是同时也因为其粒度很小,所以会占用很多空间,因此使用时需要综合考虑,是否真的需要这种空间换取时间的做法
2.3 其他解决方案
1、另外还可以通过修改lucene源码的方式来跳过特殊字符过滤
2、也可以用一些第三方插件来解决,比如elasticsearch-analysis-hanlp
3、同时考虑到java中有转义符的存在,推测es中应该也有类似的处理,但是尝试使用转义符插入数据会报错,可能用法不对
如果有知道其他方式的小伙伴也可以留言告诉我