我想使用 ElasticSearch 来搜索文件名(而不是文件的内容(。因此,我需要找到文件名的一部分(完全匹配,没有模糊搜索(。
例:
我有以下名称的文件:
My_first_file_created_at_2012.01.13.doc
My_second_file_created_at_2012.01.13.pdf
Another file.txt
And_again_another_file.docx
foo.bar.txt
现在我想搜索2012.01.13
以获取前两个文件。
搜索file
或ile
应返回除最后一个文件名之外的所有文件名。
如何使用 ElasticSearch 实现这一点?
这是我测试过的,但它总是返回零结果:
curl -X DELETE localhost:9200/files
curl -X PUT localhost:9200/files -d '
{
"settings" : {
"index" : {
"analysis" : {
"analyzer" : {
"filename_analyzer" : {
"type" : "custom",
"tokenizer" : "lowercase",
"filter" : ["filename_stop", "filename_ngram"]
}
},
"filter" : {
"filename_stop" : {
"type" : "stop",
"stopwords" : ["doc", "pdf", "docx"]
},
"filename_ngram" : {
"type" : "nGram",
"min_gram" : 3,
"max_gram" : 255
}
}
}
}
},
"mappings": {
"files": {
"properties": {
"filename": {
"type": "string",
"analyzer": "filename_analyzer"
}
}
}
}
}
'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_first_file_created_at_2012.01.13.doc" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_second_file_created_at_2012.01.13.pdf" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "Another file.txt" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "And_again_another_file.docx" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "foo.bar.txt" }'
curl -X POST "http://localhost:9200/files/_refresh"
FILES='
http://localhost:9200/files/_search?q=filename:2012.01.13
'
for file in ${FILES}
do
echo; echo; echo ">>> ${file}"
curl "${file}&pretty=true"
done
粘贴的内容有各种问题:
1( 映射不正确
创建索引时,请指定:
"mappings": {
"files": {
但你的类型实际上是file
,而不是files
. 如果您检查了映射,您将立即看到:
curl -XGET 'http://127.0.0.1:9200/files/_mapping?pretty=1'
# {
# "files" : {
# "files" : {
# "properties" : {
# "filename" : {
# "type" : "string",
# "analyzer" : "filename_analyzer"
# }
# }
# },
# "file" : {
# "properties" : {
# "filename" : {
# "type" : "string"
# }
# }
# }
# }
# }
2( 分析仪定义不正确
您已经指定了lowercase
分词器,但它删除了任何不是字母的内容(请参阅文档(,因此您的数字将被完全删除。
您可以使用分析 API 进行检查:
curl -XGET 'http://127.0.0.1:9200/_analyze?pretty=1&text=My_file_2012.01.13.doc&tokenizer=lowercase'
# {
# "tokens" : [
# {
# "end_offset" : 2,
# "position" : 1,
# "start_offset" : 0,
# "type" : "word",
# "token" : "my"
# },
# {
# "end_offset" : 7,
# "position" : 2,
# "start_offset" : 3,
# "type" : "word",
# "token" : "file"
# },
# {
# "end_offset" : 22,
# "position" : 3,
# "start_offset" : 19,
# "type" : "word",
# "token" : "doc"
# }
# ]
# }
3( 搜索中的 Ngram
在索引分析器和搜索分析器中包含 ngram 令牌筛选器。 这对于索引分析器来说很好,因为您希望对 ngram 编制索引。 但是当你搜索时,你想搜索完整的字符串,而不是每个 ngram。
例如,如果使用长度为 1 到 4 的 ngram 索引"abcd"
,则最终将得到以下标记:
a b c d ab bc cd abc bcd
但是,如果您搜索"dcba"
(应该不匹配(,并且还使用 ngram 分析搜索词,那么您实际上是在搜索:
d c b a dc cb ba dbc cba
所以a
、b
、c
和d
都会匹配!
溶液
首先,您需要选择合适的分析仪。 您的用户可能会搜索字词、数字或日期,但他们可能不希望ile
与file
匹配。 相反,使用边缘 ngram 可能更有用,它将 ngram 锚定到每个单词的开头(或结尾(。
另外,为什么要排除docx
等?用户肯定想要搜索文件类型吗?
因此,让我们通过删除任何不是字母或数字的内容(使用模式标记器(将每个文件名分解为较小的标记:
My_first_file_2012.01.13.doc
=> my first file 2012 01 13 doc
然后,对于索引分析器,我们还将在每个令牌上使用边缘 ngram:
my => m my
first => f fi fir firs first
file => f fi fil file
2012 => 2 20 201 201
01 => 0 01
13 => 1 13
doc => d do doc
我们按如下方式创建索引:
curl -XPUT 'http://127.0.0.1:9200/files/?pretty=1' -d '
{
"settings" : {
"analysis" : {
"analyzer" : {
"filename_search" : {
"tokenizer" : "filename",
"filter" : ["lowercase"]
},
"filename_index" : {
"tokenizer" : "filename",
"filter" : ["lowercase","edge_ngram"]
}
},
"tokenizer" : {
"filename" : {
"pattern" : "[^\p{L}\d]+",
"type" : "pattern"
}
},
"filter" : {
"edge_ngram" : {
"side" : "front",
"max_gram" : 20,
"min_gram" : 1,
"type" : "edgeNGram"
}
}
}
},
"mappings" : {
"file" : {
"properties" : {
"filename" : {
"type" : "string",
"search_analyzer" : "filename_search",
"index_analyzer" : "filename_index"
}
}
}
}
}
'
现在,测试我们的分析仪是否正常工作:
filename_search:
curl -XGET 'http://127.0.0.1:9200/files/_analyze?pretty=1&text=My_first_file_2012.01.13.doc&analyzer=filename_search'
[results snipped]
"token" : "my"
"token" : "first"
"token" : "file"
"token" : "2012"
"token" : "01"
"token" : "13"
"token" : "doc"
filename_index:
curl -XGET 'http://127.0.0.1:9200/files/_analyze?pretty=1&text=My_first_file_2012.01.13.doc&analyzer=filename_index'
"token" : "m"
"token" : "my"
"token" : "f"
"token" : "fi"
"token" : "fir"
"token" : "firs"
"token" : "first"
"token" : "f"
"token" : "fi"
"token" : "fil"
"token" : "file"
"token" : "2"
"token" : "20"
"token" : "201"
"token" : "2012"
"token" : "0"
"token" : "01"
"token" : "1"
"token" : "13"
"token" : "d"
"token" : "do"
"token" : "doc"
好的 - 似乎工作正常。 因此,让我们添加一些文档:
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_first_file_created_at_2012.01.13.doc" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_second_file_created_at_2012.01.13.pdf" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "Another file.txt" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "And_again_another_file.docx" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "foo.bar.txt" }'
curl -X POST "http://localhost:9200/files/_refresh"
并尝试搜索:
curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1' -d '
{
"query" : {
"text" : {
"filename" : "2012.01"
}
}
}
'
# {
# "hits" : {
# "hits" : [
# {
# "_source" : {
# "filename" : "My_second_file_created_at_2012.01.13.pdf"
# },
# "_score" : 0.06780553,
# "_index" : "files",
# "_id" : "PsDvfFCkT4yvJnlguxJrrQ",
# "_type" : "file"
# },
# {
# "_source" : {
# "filename" : "My_first_file_created_at_2012.01.13.doc"
# },
# "_score" : 0.06780553,
# "_index" : "files",
# "_id" : "ER5RmyhATg-Eu92XNGRu-w",
# "_type" : "file"
# }
# ],
# "max_score" : 0.06780553,
# "total" : 2
# },
# "timed_out" : false,
# "_shards" : {
# "failed" : 0,
# "successful" : 5,
# "total" : 5
# },
# "took" : 4
# }
成功!
#### 更新 ####
我意识到搜索2012.01
将同时匹配2012.01.12
和2012.12.01
因此我尝试将查询更改为使用文本短语查询。但是,这不起作用。 事实证明,边缘 ngram 过滤器增加了每个 ngram 的位置计数(而我本以为每个 ngram 的位置与单词开头的位置相同(。
上面第(3(点中提到的问题只是在使用尝试匹配任何令牌的query_string
、field
或text
查询时的问题。 但是,对于text_phrase
查询,它会尝试以正确的顺序匹配所有令牌。
要演示此问题,请使用其他日期为另一个文档编制索引:
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_third_file_created_at_2012.12.01.doc" }'
curl -X POST "http://localhost:9200/files/_refresh"
并执行与上述相同的搜索:
curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1' -d '
{
"query" : {
"text" : {
"filename" : {
"query" : "2012.01"
}
}
}
}
'
# {
# "hits" : {
# "hits" : [
# {
# "_source" : {
# "filename" : "My_third_file_created_at_2012.12.01.doc"
# },
# "_score" : 0.22097087,
# "_index" : "files",
# "_id" : "xmC51lIhTnWplOHADWJzaQ",
# "_type" : "file"
# },
# {
# "_source" : {
# "filename" : "My_first_file_created_at_2012.01.13.doc"
# },
# "_score" : 0.13137488,
# "_index" : "files",
# "_id" : "ZUezxDgQTsuAaCTVL9IJgg",
# "_type" : "file"
# },
# {
# "_source" : {
# "filename" : "My_second_file_created_at_2012.01.13.pdf"
# },
# "_score" : 0.13137488,
# "_index" : "files",
# "_id" : "XwLNnSlwSeyYtA2y64WuVw",
# "_type" : "file"
# }
# ],
# "max_score" : 0.22097087,
# "total" : 3
# },
# "timed_out" : false,
# "_shards" : {
# "failed" : 0,
# "successful" : 5,
# "total" : 5
# },
# "took" : 5
# }
第一个结果有一个日期2012.12.01
它不是2012.01
的最佳匹配。 因此,为了仅匹配该确切短语,我们可以执行以下操作:
curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1' -d '
{
"query" : {
"text_phrase" : {
"filename" : {
"query" : "2012.01",
"analyzer" : "filename_index"
}
}
}
}
'
# {
# "hits" : {
# "hits" : [
# {
# "_source" : {
# "filename" : "My_first_file_created_at_2012.01.13.doc"
# },
# "_score" : 0.55737644,
# "_index" : "files",
# "_id" : "ZUezxDgQTsuAaCTVL9IJgg",
# "_type" : "file"
# },
# {
# "_source" : {
# "filename" : "My_second_file_created_at_2012.01.13.pdf"
# },
# "_score" : 0.55737644,
# "_index" : "files",
# "_id" : "XwLNnSlwSeyYtA2y64WuVw",
# "_type" : "file"
# }
# ],
# "max_score" : 0.55737644,
# "total" : 2
# },
# "timed_out" : false,
# "_shards" : {
# "failed" : 0,
# "successful" : 5,
# "total" : 5
# },
# "took" : 7
# }
或者,如果您仍然想匹配所有 3 个文件(因为用户可能会记住文件名中的某些单词,但顺序错误(,您可以运行这两个查询,但增加文件名的重要性,该文件名的顺序正确:
curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1' -d '
{
"query" : {
"bool" : {
"should" : [
{
"text_phrase" : {
"filename" : {
"boost" : 2,
"query" : "2012.01",
"analyzer" : "filename_index"
}
}
},
{
"text" : {
"filename" : "2012.01"
}
}
]
}
}
}
'
# [Fri Feb 24 16:31:02 2012] Response:
# {
# "hits" : {
# "hits" : [
# {
# "_source" : {
# "filename" : "My_first_file_created_at_2012.01.13.doc"
# },
# "_score" : 0.56892186,
# "_index" : "files",
# "_id" : "ZUezxDgQTsuAaCTVL9IJgg",
# "_type" : "file"
# },
# {
# "_source" : {
# "filename" : "My_second_file_created_at_2012.01.13.pdf"
# },
# "_score" : 0.56892186,
# "_index" : "files",
# "_id" : "XwLNnSlwSeyYtA2y64WuVw",
# "_type" : "file"
# },
# {
# "_source" : {
# "filename" : "My_third_file_created_at_2012.12.01.doc"
# },
# "_score" : 0.012931341,
# "_index" : "files",
# "_id" : "xmC51lIhTnWplOHADWJzaQ",
# "_type" : "file"
# }
# ],
# "max_score" : 0.56892186,
# "total" : 3
# },
# "timed_out" : false,
# "_shards" : {
# "failed" : 0,
# "successful" : 5,
# "total" : 5
# },
# "took" : 4
# }
这是因为使用了分词器。
http://www.elasticsearch.org/guide/reference/index-modules/analysis/lowercase-tokenizer.html
小写分词器在单词边界上拆分,因此 2012.01.13 将索引为"2012"、"01"和"13"。搜索字符串"2012.01.13"显然不匹配。
一种选择是在搜索中添加标记化。因此,搜索"2012.01.13"将被标记化为与索引中相同的标记,并且它将匹配。这也很方便,因为您不需要总是在代码中小写搜索。
第二种选择是使用 n 元语法标记器而不是过滤器。这意味着它将忽略单词边界(您还将获得"_"(,但是您可能会遇到大小写不匹配的问题,这可能是您首先添加小写标记器的原因。
我没有使用 ES 的经验,但在 Solr 中,您需要将字段类型指定为文本。您的字段类型为字符串而不是文本。字符串字段不进行分析,而是逐字存储和索引。试一试,看看它是否有效。
properties": {
"filename": {
"type": "string",
"analyzer": "filename_analyzer"
}