作者:来自 Elastic Alexander Marquardt

探索在 Elasticsearch 中编码和匹配二进制数据的六种方法,包括术语编码(我喜欢的方法)、布尔编码、稀疏位位置编码、具有精确匹配的整数编码、具有脚本按位匹配的整数编码以及使用 ESQL 进行按位匹配的整数编码,并提供实际示例和用例。

简介

二进制编码是现代应用中的一项重要技术,尤其是在物联网设备监控等领域,这些领域需要持续处理大量二进制传感器数据或操作标志。高效管理和搜索此类数据对于实时分析和决策至关重要。为了实现这一点,按位匹配是一种基于二进制值进行过滤的强大工具,可以实现精确的数据提取。通过正确的数据建模,Elasticsearch 不仅支持按位匹配,而且性能极佳。

在撰写本文时,Elasticsearch 没有用于按位匹配的原生运算符,而 Lucene 也不直接支持按位匹配。为了克服这一限制,本文介绍了 Elasticsearch 中的六种二进制编码和按位匹配方法:术语编码(我首选的方法)、布尔编码、稀疏位位置编码、带精确匹配的整数编码、带脚本按位匹配的整数编码以及带 ESQL 的按位匹配整数编码。

术语编码 - terms encoding

使用术语进行二进制表示可利用 Elasticsearch 优化的基于术语的查询。此方法涉及将每个位表示为存储在关键字字段中的术语。

术语编码的优点

基于术语的方法允许 Elasticsearch 使用优化的数据结构,从而即使对于大型数据集也能实现高效查询。此外,此方法在需要频繁查询不同位组合的情况下具有很好的扩展性,因为每个位都被视为可以索引和搜索的独立实体。

术语编码的缺点

此方法要求在将数据存储在 Elasticsearch 中之前对数据进行预处理以将其转换为术语编码格式。此外,按位查询必须构建为一系列术语匹配,如下所示。此外,由于每个二进制序列由多个术语表示,因此这可能会占用比整数表示所需的更多存储空间。

设置术语编码的环境

定义关键字表示的映射:

PUT test_terms_encoding
{
 "mappings": {
   "properties": {
     "terms_encoded_bits": {
       "type": "keyword"
     }
   }
 }
}

使用术语编码对文档进行索引

使用二进制位表示对文档进行索引:

POST test_terms_encoding/_doc/1
{
 "terms_encoded_bits": ["b3=0", "b2=1", "b1=1", "b0=0"] // binary 0110
}
POST test_terms_encoding/_doc/2
{
 "terms_encoded_bits": ["b3=1", "b2=0", "b1=1", "b0=0"] // binary 1010
}

使用术语编码进行查询

查询 b3 为真、b0 为假的文档(即上面的 _id=2 的文档)

GET test_terms_encoding/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "terms_encoded_bits": "b3=1"
          }
        },
        {
          "term": {
            "terms_encoded_bits": "b0=0"
          }
        }
      ]
    }
  }
}

布尔编码

此方法对每个位使用单独的布尔字段,提供对特定位的清晰直接访问。

布尔编码的优点

布尔编码方法具有 “术语编码” 方法的所有优点,有些人可能发现这种方法更直观。对于某些数据集,它可能需要的存储空间也略少,因为每个字段只存储一个布尔值,而不是字符串。

布尔编码的缺点

这具有 “术语编码” 方法列出的所有缺点。这种方法的另一个缺点是,由于我们为每个位创建了一个新字段,这会导致映射爆炸。

设置布尔编码的环境

使用布尔字段定义映射:

PUT test_boolean_encoding
{
  "mappings": {
    "properties": {
      "b3": { "type": "boolean" },
      "b2": { "type": "boolean" },
      "b1": { "type": "boolean" },
      "b0": { "type": "boolean" }
    }
  }
}

使用布尔编码对文档进行索引

使用每个位的布尔值对文档进行索引:

POST test_boolean_encoding/_doc/1
{
 "b3": false, 
 "b2": true,
 "b1": true,
 "b0": false
} // binary 0110 – integer 6
POST test_boolean_encoding/_doc/2
{
 "b3": true,
 "b2": false,
 "b1": true,
 "b0": false
} // binary 1010 – integer 10

使用布尔编码进行查询

要查询 b3 为真、b0 为假的文档(即上面的 _id=2 的文档):

GET test_boolean_encoding/_search
{
 "query": {
   "bool": {
     "filter": [
       {
         "term": {
           "b3": true
         }
       },
       {
         "term": {
           "b0": false
         }
       }
     ]
   }
 }
}

稀疏位位置编码 - Sparse Bit Position Encoding

此方法仅对数组中设置为 true 的位的位置进行编码。

稀疏位位置编码的优点

如果绝大多数文档通常没有任何位设置为 true,那么与以前的方法相比,这种方法在存储方面可能更有效。例如,你可以想象,表示警告标志的位很少为 true 的数据集,这将导致位位置编码数组为空(因此节省空间),而这些数组对于很大一部分文档而言都是空的。

稀疏位位置编码的缺点

此方法的一个缺点是,查询不为 true 的位需要使用 must_not 运算符。但是,使用 must_not 进行查询可能会导致性能开销,因为 Elasticsearch 需要扫描文档以验证某些值的缺失,这比直接查询特定术语的存在效率更低。在大型数据集中,这可能会减慢处理速度。另一个缺点是,如果数据始终有许多位设置为 true,那么这将需要一长串整数来表示,这会增加存储要求。最后,与其他方法一样,此方法需要预处理数据,将其转换为稀疏位位置编码,然后再将其存储在 Elasticsearch 中。

设置稀疏位位置编码的环境

定义整数数组的映射:

PUT test_sparse_encoding
{
  "mappings": {
    "properties": {
      "sparse_bit_positions": {
        "type": "integer"
      }
    }
  }
}

使用稀疏位位置编码对文档进行索引

使用位位置编码为整数对文档进行索引:

POST test_sparse_encoding/_doc/1
{
  "sparse_bit_positions": [2, 1] // binary 0110
}
POST test_sparse_encoding/_doc/2
{
  "sparse_bit_positions": [3, 1] // binary 1010
}

使用稀疏位位置编码进行查询

要查询 b3 为真、b0 为假的文档(即上面的 _id=2 的文档):

GET test_sparse_encoding/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "sparse_bit_positions": 3
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "sparse_bit_positions": 0
          }
        }
      ]
    }
  }
}

带精确匹配的整数编码

在这种方法中,二进制值被编码为整数。这是一种直观的方法,特别是当你需要高效地存储和查询完整的二进制序列(即整数)时。

带精确匹配的整数编码的优点

在迄今为止讨论的方法中,这种方法最有可能直接映射到源系统中数据存储的方式,源系统通常将二进制序列表示为整数。因此,使用这种方法存储文档可能比其他方法需要更少的预处理。

带精确匹配的整数编码的缺点

这种方法仅讨论表示二进制序列的整数值的精确匹配。它不解决整数内的按位匹配问题。它还要求你在将二进制值存储在 Elasticsearch 中之前将其转换为整数。

设置整数编码的环境

定义整数编码的映射:

PUT test_integer_encoding
{
 "mappings": {
   "properties": {
     "integer_representation": {
       "type": "integer"
     }
   }
 }
}

使用整数编码对文档进行索引

通过将二进制序列转换为整数来对文档进行索引:

POST test_integer_encoding/_doc/1
{
 "integer_representation": 6  // binary 0110
}
POST test_integer_encoding/_doc/2
{
 "integer_representation": 10 // binary 1010
}

使用整数编码进行查询

以下查询检索二进制表示等于特定值的文档 - 在此示例中,文档包含整数表示 0110 且 _id=1:

GET test_integer_encoding/_search
{
 "query": {
   "term": {
     "integer_representation": 6 // binary 0110
   }
 }
}

使用脚本按位匹配进行整数编码

在这种方法中,我们扩展了将二进制值编码为整数的概念,并利用 scripted query 功能来查询整数值中设置的各个位。本文将对这种方法进行讨论,以保证完整性,但请记住,大量使用脚本查询将给集群带来额外的工作负载,并且可能比其他方法更慢、效率更低。

使用脚本按位匹配进行整数编码的优点

这种方法具有“使用精确匹配进行整数编码”方法的优点。另一个优点是可以匹配各个位。

使用脚本按位匹配进行整数编码的缺点

这种按位匹配方法没有利用 Elasticsearch 构建的数据结构来确保快速高效的查询。因此,这种方法可能会导致查询速度变慢,并且比前面提到的方法需要更多的资源。因此,我通常会推荐前面讨论的方法。

设置和索引文档

在本节中,我们将使用在第二个名为 “精确匹配的整数编码” 的章节中填充的相同索引。

查询

要查询 b3 为真且 b0 为假的文档(即上面的 _id=2 的文档),我们可以使用脚本查询。以下查询满足这些要求,并附有注释以解释逻辑:

GET test_integer_encoding/_search
{
  "query": {
    "bool": {
      "filter": {
        "script": {
          "script": {
            "source": """
            // b3 is true 
            // i.e. "integer_representation" AND 1000 == 1000
            // Keep in mind that 1000 binary == 8 in integer
            ((doc['integer_representation'].value & 8) == 8) && 
            
            // b0 false 
            // i.e. "integer_representation" AND 0001 == 0
            ((doc['integer_representation'].value & 1) == 0)"""   
          }
        }
      }
    }
  }
}

使用 ES|QL 进行整数编码以进行按位匹配

与 “使用脚本按位匹配进行整数编码” 方法一样,此方法也可以匹配单个位,但它利用的是 ES|QL 而不是脚本查询。本文将对此方法进行讨论,以保证完整性,但大量使用此方法可能会在集群上产生额外的工作负载,并且可能比其他方法更慢、效率更低。

使用 ES|QL 进行整数编码以进行按位匹配的优势

此方法具有与 “使用脚本按位匹配进行整数编码” 相同的优势。另一个优势是它利用了在设计时考虑了性能的 ES|QL。

使用 ES|QL 进行整数编码以进行按位匹配的缺点

尽管此方法利用了 ES|QL,但它无法直接使用预构建的数据结构进行按位匹配。因此,此方法可能会导致查询速度变慢,并且比许多其他方法需要更多的资源。

设置和索引文档

在本节中,我们将使用在第二个名为“精确匹配的整数编码”的章节中填充的相同索引。

查询

要查询 b3 为真且 b0 为假的文档(即上面的 _id=2 的文档),我们可以使用 ES|QL。以下查询满足这些要求,并附有注释以解释逻辑:

POST /_query?format=txt
{
  "query": """
  FROM test_integer_encoding METADATA _index, _id
  // The following uses division to shift the bit we are interested 
  // in, to the right. ie. dividing by 8 (or b1000 - corresponding to b3) 
  // shifts b3 to the least significant bit position, or b0. 
  // Additionally, the rightmost bits are dropped.
  // Then the modulus by 2 checks if the new (post shift) 
  // value of b0 (formerly b3)is odd or even (1 or 0)
  | WHERE (integer_representation / 8 % 2 == 1)  // b3 is true
  | WHERE (integer_representation / 1 % 2 == 0)  // b0 is false
  | KEEP _id, integer_representation
  """
}

结论

在本文中,我们探讨了六种按位匹配方法 —— 术语编码(我首选的方法)、布尔编码、稀疏位位置编码、精确匹配的整数编码、脚本按位匹配的整数编码以及使用 ES|QL 进行按位匹配的整数编码。这展示了如何应用不同的方法来有效地处理 Elasticsearch 中的按位匹配。每种方法都有其优点和权衡,具体取决于应用程序的特定要求。

对于需要单独位匹配的场景,基于术语和布尔字段的方法效果很好,应该是有效的。在整数数组中表示真实位位置为稀疏位序列提供了一种紧凑而灵活的解决方案。将二进制序列编码为整数可能适合整个序列操作,但代价是失去有效查询单个位的能力。我们还可以使用脚本查询或 ES|QL 查询整数中的单个位,但这些方法可能不如其他方法有效。

致谢

感谢 Honza Kral 提出使用术语和整数对单个位进行编码的想法,感谢 Alexis Charveriat 建议使用 ES|QL 进行按位匹配的方法,感谢 Carly Richmod 在技术审查过程中提出的宝贵建议。

准备好自己尝试一下了吗?开始免费试用

想要获得 Elastic 认证?了解下一期 Elasticsearch 工程师培训何时开始!

原文:Efficient bitwise matching in Elasticsearch - Search Labs

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部