给定主题图像" 100111..10"我们希望在海明距离为8 的Elasticsearch索引中找到所有类似的图像哈希值。
当然,查询可以返回距离大于8的图像,Elasticsearch或外部的脚本可以过滤结果集。但总搜索时间必须在1秒左右。
每个文档都有嵌套的images
字段,其中包含图像哈希:
{
"images": {
"type": "nested",
"properties": {
"pHashFingerprint": {"index": "not_analysed", "type": "string"}
}
}
}
事实: Elasticsearch模糊查询仅支持最大2的Levenshtein距离。
我们使用自定义标记生成器将64位字符串拆分为4组16位,并使用4个模糊查询进行4组搜索。
分析仪:
{
"analysis": {
"analyzer": {
"split4_fingerprint_analyzer": {
"type": "custom",
"tokenizer": "split4_fingerprint_tokenizer"
}
},
"tokenizer": {
"split4_fingerprint_tokenizer": {
"type": "pattern",
"group": 0,
"pattern": "([01]{16})"
}
}
}
}
然后新的字段映射:
"index_analyzer": "split4_fingerprint_analyzer",
然后查询:
{
"query": {
"filtered": {
"query": {
"nested": {
"path": "images",
"query": {
"bool": {
"minimum_should_match": 2,
"should": [
{
"fuzzy": {
"phashFingerprint.split4": {
"value": "0010100100111001",
"fuzziness": 2
}
}
},
{
"fuzzy": {
"phashFingerprint.split4": {
"value": "1010100100111001",
"fuzziness": 2
}
}
},
{
"fuzzy": {
"phashFingerprint.split4": {
"value": "0110100100111001",
"fuzziness": 2
}
}
},
{
"fuzzy": {
"phashFingerprint.split4": {
"value": "1110100100111001",
"fuzziness": 2
}
}
}
]
}
}
}
},
"filter": {}
}
}
}
请注意,我们会返回具有匹配图像的文档,而不是图像本身,但这不会改变很多事情。
问题是,即使在添加其他特定于域的过滤器以减少初始设置之后,此查询也会返回数十万个结果。脚本有太多的工作来再次计算汉明距离,因此查询可能需要几分钟。
正如预期的那样,如果将minimum_should_match
增加到3和4,则只返回必须找到的图像子集,但结果集很小且很快。 95%的所需图片会在minimum_should_match
== 3时返回,但我们需要100%(或99.9%)与minimum_should_match
== 2一样。
我们用n-gram尝试了类似的方法,但仍然没有太多成功的类似方式。
其他数据结构和查询的任何解决方案?
修改 :
我们注意到,我们的评估过程中存在错误,minimum_should_match
== 2会返回100%的结果。但是,之后的处理时间平均为5秒。我们将看看脚本是否值得优化。
答案 0 :(得分:16)
我已经模拟并实现了一个可能的解决方案,避免了所有昂贵的“模糊”查询。而是在索引时,您从这64位中取N
个M
位的随机样本。我想这是Locality-sensitive hashing的一个例子。因此,对于每个文档(以及查询时),始终从相同的位位置获取样本编号x
,以便在文档之间进行一致的散列。
查询使用term
的{{1}}子句中的bool query
过滤器,其should
阈值相对较低。较低的阈值对应于较高的“模糊性”。不幸的是,您需要重新索引所有图像以测试此方法。
我认为minimum_should_match
个查询效果不佳,因为平均每个过滤条件都匹配{ "term": { "phash.0": true } }
个文档。每个样本使用16位/样本匹配50%
个文档。
我使用以下设置运行测试:
2^-16 = 0.0015%
- "0"
)"ff"
类型,short
)doc_values = true
和样本来最小化,只有原始的二进制哈希值)_source
= 150(满分1024)您可以使用更少的样本获得更快的速度和更低的磁盘使用率,但是汉明距离8和9之间的文档分离得不是很好(根据我的模拟)。 1024似乎是minimum_should_match
子句的最大数量。
测试在单个Core i5 3570K,24 GB RAM,8 GB for ES,版本1.7.1上运行。来自500个查询的结果(请参阅下面的说明,结果过于乐观):
should
我将测试它如何扩展到1500万个文档,但每个索引生成和存储100万个文档需要3个小时。
你应该测试或计算你应该设置Mean time: 221.330 ms
Mean docs: 197
Percentiles:
1st = 140.51ms
5th = 150.17ms
25th = 172.29ms
50th = 207.92ms
75th = 233.25ms
95th = 296.27ms
99th = 533.88ms
的低点,以便在错过的匹配和不正确的匹配之间获得所需的权衡,这取决于哈希的分布。
示例查询(显示1024个字段中的3个):
minimum_should_match
编辑:当我开始做进一步的基准测试时,我注意到我已经为不同的索引生成了太不相似的哈希值,因此从那些搜索中搜索导致零匹配。新生成的文档会产生大约150 - 250个匹配/索引/查询,并且应该更加真实。
之前的图表中显示了新的结果,我有ES的4 GB内存和OS的剩余20 GB。搜索1 - 3个索引具有良好的性能(中位时间0.1 - 0.2秒),但搜索超过这个导致大量的磁盘IO和查询开始需要9 - 11秒!这可以通过减少散列样本来规避,但随后召回并且精确率不会那么好,或者你可以拥有一台64 GB RAM的机器,看看你能得到多远。
编辑2:我使用{
"bool": {
"should": [
{
"filtered": {
"filter": {
"term": {
"0": -12094,
"_cache": false
}
}
}
},
{
"filtered": {
"filter": {
"term": {
"_cache": false,
"1": -20275
}
}
}
},
{
"filtered": {
"filter": {
"term": {
"ff": 15724,
"_cache": false
}
}
}
}
],
"minimum_should_match": 150
}
}
重新生成数据而不存储哈希样本(仅原始哈希),这将存储空间减少了60%,达到约6.7 GB /索引( 100万个文档)。这不会影响较小数据集的查询速度,但是当RAM不足并且必须使用磁盘时,查询速度提高了大约40%。
编辑3:我在一组3000万个文档中测试了_source: false
搜索,编辑距离为2,并将其与256个随机哈希样本进行比较,以获得近似结果。在这些条件下,方法的速度大致相同,但fuzzy
给出了精确的结果,并且不需要额外的磁盘空间。我认为这种方法仅适用于“非常模糊”的查询,例如汉明距离大于3。
答案 1 :(得分:8)
即使在笔记本电脑的GeForce 650M显卡上,我也实现了CUDA方法并取得了一些不错的效果。使用Thrust库可以轻松实现。我希望代码没有错误(我没有彻底测试它),但它不应该影响基准测试结果。至少我在停止high-precision timer之前致电thrust::system::cuda::detail::synchronize()
。
typedef unsigned __int32 uint32_t;
typedef unsigned __int64 uint64_t;
// Maybe there is a simple 64-bit solution out there?
__host__ __device__ inline int hammingWeight(uint32_t v)
{
v = v - ((v>>1) & 0x55555555);
v = (v & 0x33333333) + ((v>>2) & 0x33333333);
return ((v + (v>>4) & 0xF0F0F0F) * 0x1010101) >> 24;
}
__host__ __device__ inline int hammingDistance(const uint64_t a, const uint64_t b)
{
const uint64_t delta = a ^ b;
return hammingWeight(delta & 0xffffffffULL) + hammingWeight(delta >> 32);
}
struct HammingDistanceFilter
{
const uint64_t _target, _maxDistance;
HammingDistanceFilter(const uint64_t target, const uint64_t maxDistance) :
_target(target), _maxDistance(maxDistance) {
}
__host__ __device__ bool operator()(const uint64_t hash) {
return hammingDistance(_target, hash) <= _maxDistance;
}
};
线性搜索就像
一样简单thrust::copy_if(
hashesGpu.cbegin(), hashesGpu.cend(), matchesGpu.begin(),
HammingDistanceFilter(target_hash, maxDistance)
)
搜索是100%准确且比我的ElasticSearch答案更快,在50毫秒内CUDA可以流过3500万个哈希值!我确定较新的桌面卡比这更快。当我们浏览越来越多的数据时,我们也会获得非常低的方差和一致的搜索时间线性增长。由于采样数据膨胀,ElasticSearch在较大的查询中遇到了错误的内存问题。
所以我在这里报告&#34;从这些N个哈希的结果,找到距离单个哈希H&#34;在8汉明距离内的那些哈希值。我跑了500次并报告了百分位数。
有一些内核启动开销,但在搜索空间超过500万次哈希后,搜索速度相当稳定,达到7亿哈希/秒。当然,要搜索的散列数的上限由GPU的RAM设置。
更新:我在GTX 1060上重新运行测试,每秒扫描大约3800万次哈希:)
答案 2 :(得分:4)
我自己开始解决这个问题了。到目前为止,我只对大约380万份文档的数据集进行了测试,我打算将其推高到数百万以上。
到目前为止我的解决方案是:
编写本机评分函数并将其注册为插件。然后在查询时调用它来调整文档的_score
值。
作为一个时髦的脚本,运行自定义评分函数所花费的时间非常不起眼,但将其作为本机评分函数编写(如这篇有点陈旧的博客帖子所示:http://www.spacevatican.org/2012/5/12/elasticsearch-native-scripts-for-dummies/)的速度要快几个数量级
我的HammingDistanceScript看起来像这样:
public class HammingDistanceScript extends AbstractFloatSearchScript {
private String field;
private String hash;
private int length;
public HammingDistanceScript(Map<String, Object> params) {
super();
field = (String) params.get("param_field");
hash = (String) params.get("param_hash");
if(hash != null){
length = hash.length() * 8;
}
}
private int hammingDistance(CharSequence lhs, CharSequence rhs){
return length - new BigInteger(lhs, 16).xor(new BigInteger(rhs, 16)).bitCount();
}
@Override
public float runAsFloat() {
String fieldValue = ((ScriptDocValues.Strings) doc().get(field)).getValue();
//Serious arse covering:
if(hash == null || fieldValue == null || fieldValue.length() != hash.length()){
return 0.0f;
}
return hammingDistance(fieldValue, hash);
}
}
此时值得一提的是,我的哈希值是十六进制编码的二进制字符串。所以,和你的一样,但是十六进制编码以减少存储空间。
另外,我期待一个param_field参数,它可以识别我想要对汉明距离做哪个字段值。您不需要这样做,但我在多个字段中使用相同的脚本,所以我这样做:)
我在这样的查询中使用它:
curl -XPOST 'http://localhost:9200/scf/_search?pretty' -d '{
"query": {
"function_score": {
"min_score": MY IDEAL MIN SCORE HERE,
"query":{
"match_all":{}
},
"functions": [
{
"script_score": {
"script": "hamming_distance",
"lang" : "native",
"params": {
"param_hash": "HASH TO COMPARE WITH",
"param_field":"phash"
}
}
}
]
}
}
}'
我希望这在某种程度上有所帮助!
如果您走这条路线可能对您有用的其他信息:
<强> 1。请记住es-plugin.properties文件
这必须编译到你的jar文件的根目录中(如果你把它放在/ src / main / resources中然后构建你的jar它会去正确的地方)。
plugin=com.example.elasticsearch.plugins.HammingDistancePlugin
name=hamming_distance
version=0.1.0
jvm=true
classname=com.example.elasticsearch.plugins.HammingDistancePlugin
java.version=1.7
elasticsearch.version=1.7.3
<强> 2。在elasticsearch.yml中引用自定义NativeScriptFactory impl
就像在老年博客上一样。
script.native:
hamming_distance.type: com.example.elasticsearch.plugins.HammingDistanceScriptFactory
如果你不这样做,它仍会出现在插件列表中(见下文),但是当你尝试使用弹性搜索无法找到它时,你会收到错误。
第3。不要使用elasticsearch插件脚本来安装它
它只是一个痛苦的屁股,它似乎只是解开你的东西 - 有点无意义。相反,只需将其粘贴在%ELASTICSEARCH_HOME%/plugins/hamming_distance
中
并重启elasticsearch。
如果一切顺利,你会在elasticsearch startup上看到它被加载:
[2016-02-09 12:02:43,765][INFO ][plugins ] [Junta] loaded [mapper-attachments, marvel, knapsack-1.7.2.0-954d066, hamming_distance, euclidean_distance, cloud-aws], sites [marvel, bigdesk]
当你拨打插件列表时,它会在那里:
curl http://localhost:9200/_cat/plugins?v
产生类似的东西:
name component version type url
Junta hamming_distance 0.1.0 j
我希望能够在接下来的一周内测试数以万计的文件。如果有帮助的话,我会尝试并记住弹出并用结果更新它。
答案 3 :(得分:1)
这是一个不优雅但非常精确的(暴力)解决方案,需要将您的功能哈希解构为单独的布尔字段,以便您可以运行如下查询:
"query": {
"bool": {
"minimum_should_match": -8,
"should": [
{ "term": { "phash.0": true } },
{ "term": { "phash.1": false } },
...
{ "term": { "phash.63": true } }
]
}
}
我不确定这会如何与fuzzy_like_this相比,但 FLT 实现被弃用的原因是它必须访问索引中的每个术语来计算编辑距离
(在此处/上方,您正在利用Lucene的底层反向索引数据结构和优化的集合操作,应该对您有利,因为您可能已经相当稀疏功能)
答案 4 :(得分:1)
我使用@ndtreviv's答案作为起点。以下是我对ElasticSearch 2.3.3的说明:
es-plugin.properties
文件现在称为plugin-descriptor.properties
您没有在NativeScriptFactory
中引用elasticsearch.yml
,而是在HammingDistanceScript
旁边创建了一个额外的课程。
import org.elasticsearch.common.Nullable;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.NativeScriptFactory;
import org.elasticsearch.script.ScriptModule;
import java.util.Map;
public class StringMetricsPlugin extends Plugin {
@Override
public String name() {
return "string-metrics";
}
@Override
public String description() {
return "";
}
public void onModule(ScriptModule module) {
module.registerScript("hamming-distance", HammingDistanceScriptFactory.class);
}
public static class HammingDistanceScriptFactory implements NativeScriptFactory {
@Override
public ExecutableScript newScript(@Nullable Map<String, Object> params) {
return new HammingDistanceScript(params);
}
@Override
public boolean needsScores() {
return false;
}
}
}
plugin-descriptor.properties
文件中引用此课程:plugin=com.example.elasticsearch.plugins. StringMetricsPlugin
name=string-metrics
version=0.1.0
jvm=true
classname=com.example.elasticsearch.plugins.StringMetricsPlugin
java.version=1.8
elasticsearch.version=2.3.3
module.registerScript("hamming-distance", HammingDistanceScriptFactory.class);
in 2。希望这有助于下一个必须处理糟糕的ES文档的可怜的灵魂。
答案 5 :(得分:1)
以下是@NikoNyrh's答案的64位解决方案。汉明距离可以通过使用具有内置__popcll CUDA函数的XOR运算符来计算。
struct HammingDistanceFilter
{
const uint64_t _target, _maxDistance;
HammingDistanceFilter(const uint64_t target, const uint64_t maxDistance) :
_target(target), _maxDistance(maxDistance) {
}
__device__ bool operator()(const uint64_t hash) {
return __popcll(_target ^ hash) <= _maxDistance;
}
};
答案 6 :(得分:1)
最近从[1]中提出的FENSHSES方法似乎是在Elasticsearch的汉明空间中进行r邻域搜索的最新方法。
[1] Mu,C,Zhao,J.,Yang,G.,Yang,B. and Yan,Z.,2019年10月。在全文搜索引擎上的汉明空间中进行快速,精确的最近邻搜索。在关于相似性搜索和应用的国际会议上(第49-56页)。湛史普林格。