图形

本用户指南部分涵盖了图形表达式背后的语法和理论。针对两个关键图形用例提供了示例:二分图推荐器事件关联时间图形查询

图形

Solr 中编入索引的日志记录和其他数据之间存在连接,这些连接可以视为分布式图形。图形表达式提供了一种识别图形中的根节点并遍历其连接的机制。图形遍历的总体目标是具体化一个特定的子图并执行链接分析以了解节点之间的连接。

在下面的几个部分中,我们将回顾 Solr 图形表达式背后的图形理论。

子图

子图是较大图形中节点和连接的一个较小子集。图形表达式允许您灵活地定义和具体化分布式索引中存储的较大图形中的子图。

子图发挥着两个重要作用

  • 它们为链接分析提供了一个局部上下文。子图的设计定义了链接分析的含义。

  • 它们提供了一个前台图形,可以与后台索引进行比较,以进行异常检测。

二分图

图形表达式可用于实现二部子图。二部图是一种将节点分成两个不同类别的图。然后可以分析这两个类别之间的链接,以研究它们之间的关系。二部图通常在协作过滤推荐系统中进行讨论。

购物篮产品之间的二部图是一个有用的示例。通过购物篮和产品之间的链接分析,我们可以确定哪些产品最常在同一个购物篮中购买。

在下面的示例中,有一个名为 baskets 的 Solr 集合,其中包含三个字段

id:唯一 ID

basket_s:购物篮 ID

product_s:产品

集合中的每条记录都表示购物篮中的一个产品。同一个篮子中的所有产品都共享相同的篮子 ID。

我们考虑一个简单的示例,其中我们希望找到一种经常与黄油一起销售的产品。为了做到这一点,我们可以创建一个包含黄油的购物篮的二部子图。我们不会将黄油本身包含在图表中,因为它无助于为黄油找到互补产品。

下面是一个以矩阵形式表示的此二部子图的示例

graph1

在此示例中,有三个购物篮由行显示:basket1、basket2、basket3。

还有三列显示的三种产品:奶酪、鸡蛋、牛奶。

每个单元格都有一个 1 或 0,表示产品是否在篮子中。

我们来看看 Solr 图形表达式如何实现此二部子图

nodes 函数用于从较大的图中实现子图。下面是一个示例节点函数,它实现了上面矩阵中显示的二部图。

nodes(baskets,
      random(baskets, q="product_s:butter", fl="basket_s", rows="3"),
      walk="basket_s->basket_s",
      fq="-product_s:butter",
      gather="product_s",
      trackTraversal="true")

让我们从random函数开始分解此示例

random(baskets, q="product_s:butter", fl="basket_s", rows="3")

random 函数使用查询 product_s:butter 搜索 baskets 集合,并返回 3 个随机样本。每个样本都包含 basket_s 字段,即篮子 ID。random 样本返回的三个篮子 ID 是图查询的根节点

nodes 函数是图查询。nodes 函数对 random 函数返回的三个根节点进行操作。它通过搜索根节点的 basket_s 字段与索引中的 basket_s 字段进行对比来“遍历”图。这将找到根篮子的所有产品记录。然后,它将“收集”在遍历中找到的记录中的 product_s 字段。应用一个过滤器,以便不会返回 product_s 字段中含有黄油的记录。

trackTraversal 标志告诉 nodes 表达式跟踪根篮子和产品之间的链接。

节点集

nodes 函数的输出是一个节点集,它表示由 nodes 函数指定的子图。节点集包含在图遍历期间收集的一组唯一节点。结果中的 node 属性是收集的节点的值。在购物篮示例中,product_s 字段位于节点属性中,因为这是在 nodes 表达式中指定要收集的内容。

购物篮图表达式的输出如下

{
  "result-set": {
    "docs": [
      {
        "node": "eggs",
        "collection": "baskets",
        "field": "product_s",
        "ancestors": [
          "basket1",
          "basket3"
        ],
        "level": 1
      },
      {
        "node": "cheese",
        "collection": "baskets",
        "field": "product_s",
        "ancestors": [
          "basket2"
        ],
        "level": 1
      },
      {
        "node": "milk",
        "collection": "baskets",
        "field": "product_s",
        "ancestors": [
          "basket1",
          "basket2"
        ],
        "level": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 12
      }
    ]
  }
}

结果中的 ancestors 属性包含子图中所有入站链接到节点的唯一、按字母顺序排列的集合。在本例中,它显示了链接到每个产品的篮子。只有当在 nodes 表达式中打开 trackTraversal 标志时,才会跟踪祖先链接。

通常执行链接分析来确定节点中心性。在分析中心性时,目标是根据节点在子图中的连接程度为每个节点分配一个权重。有不同类型的节点中心性。图表达式非常有效地计算入度中心性(入度)。

入度中心性通过计算到每个节点的入站链接数来计算。为了简单起见,本文档有时会将入度简单地称为度。

回到购物篮示例

graph1

我们可以通过对各列求和来计算图中产品的度

cheese: 1
eggs:   2
milk:   2

从度计算中,我们知道鸡蛋牛奶在有黄油的购物篮中出现的频率高于奶酪

节点函数可以通过添加 count(*) 聚合来计算度中心性,如下所示

nodes(baskets,
      random(baskets, q="product_s:butter", fl="basket_s", rows="3"),
      walk="basket_s->basket_s",
      fq="-product_s:butter",
      gather="product_s",
      trackTraversal="true",
      count(*))

此图表达式输出如下

{
  "result-set": {
    "docs": [
      {
        "node": "eggs",
        "count(*)": 2,
        "collection": "baskets",
        "field": "product_s",
        "ancestors": [
          "basket1",
          "basket3"
        ],
        "level": 1
      },
      {
        "node": "cheese",
        "count(*)": 1,
        "collection": "baskets",
        "field": "product_s",
        "ancestors": [
          "basket2"
        ],
        "level": 1
      },
      {
        "node": "milk",
        "count(*)": 2,
        "collection": "baskets",
        "field": "product_s",
        "ancestors": [
          "basket1",
          "basket2"
        ],
        "level": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 17
      }
    ]
  }
}

count(*) 聚合计算“收集的”节点,在本例中为 product_s 字段中的值。请注意,count(*) 结果与祖先数量相同。这种情况始终如此,因为节点函数在计算收集的节点之前会首先对边进行去重。因此,count(*) 聚合始终计算收集的节点的入度中心性。

点积

入度与二部图推荐器和点积之间存在直接关系。一旦我们为黄油添加一列,就可以在我们的工作示例中清楚地看到这种关系

graph2

如果我们计算黄油列与其他产品列之间的点积,你会发现点积在每种情况下都等于入度。这告诉我们,使用最大内积相似性的最近邻搜索将选择入度最高的列。

限制购物篮出度

通过限制购物篮的出度可以增强推荐。出度是图中节点的出站链接数。在购物篮示例中,从购物篮出站的链接链接到产品。因此,限制出度将限制购物篮的大小。

为什么限制购物篮的大小会产生更强的推荐?要回答这个问题,可以将每个购物篮视为对与黄油搭配的产品进行投票。在有两名候选人的选举中,如果你投票给两名候选人,那么选票将相互抵消,不起作用。但如果你只投票给一名候选人,你的选票将影响结果。相同的原则也适用于推荐。随着购物篮投票给更多产品,它对任何一种产品的推荐强度都会被稀释。只包含黄油和另一件商品的购物篮会更强烈地推荐该商品。

maxDocFreq 参数可用于将图“遍历”限制为仅包含在索引中出现一定次数的购物篮。由于索引中购物篮 ID 的每次出现都是指向产品的链接,因此限制购物篮 ID 的文档频率将限制购物篮的出度。maxDocFreq 参数按分片应用。如果只有一个分片或文档按购物篮 ID 共置,则 maxDocFreq 将是确切计数。否则,它将返回最大大小为 numShards * maxDocFreq 的购物篮。

以下示例显示了应用于 nodes 表达式的 maxDocFreq 参数。

nodes(baskets,
      random(baskets, q="product_s:butter", fl="basket_s", rows="3"),
      walk="basket_s->basket_s",
      maxDocFreq="5",
      fq="-product_s:butter",
      gather="product_s",
      trackTraversal="true",
      count(*))

节点评分

节点的度数描述了子图中链接到它的节点数。但这并不能告诉我们该节点是否特别适合这个子图,或者它只是整个图中一个非常频繁的节点。在子图中频繁出现但在整个图中不频繁出现的节点可以被认为与子图更相关

搜索索引包含有关每个节点在整个索引中出现频率的信息。使用类似于tf-idf文档评分的技术,图表达式可以将节点的度数与其在索引中的逆向文档频率相结合,以确定相关性得分。

scoreNodes函数对节点进行评分。以下是将scoreNodes函数应用于购物篮节点集的示例。

scoreNodes(nodes(baskets,
                 random(baskets, q="product_s:butter", fl="basket_s", rows="3"),
                 walk="basket_s->basket_s",
                 fq="-product_s:butter",
                 gather="product_s",
                 trackTraversal="true",
                 count(*)))

输出现在包括一个nodeScore属性。在下面的输出中,请注意鸡蛋的nodeScore比牛奶高,即使它们具有相同的count(*)。这是因为牛奶在整个索引中比鸡蛋出现得更频繁。nodeScore函数添加的docFreq属性显示了索引中的文档频率。由于较低的docFreq,鸡蛋被认为与这个子图更相关,并且是与黄油搭配的更好的推荐。

{
  "result-set": {
    "docs": [
      {
        "node": "eggs",
        "nodeScore": 3.8930247,
        "field": "product_s",
        "numDocs": 10,
        "level": 1,
        "count(*)": 2,
        "collection": "baskets",
        "ancestors": [
          "basket1",
          "basket3"
        ],
        "docFreq": 2
      },
      {
        "node": "milk",
        "nodeScore": 3.0281217,
        "field": "product_s",
        "numDocs": 10,
        "level": 1,
        "count(*)": 2,
        "collection": "baskets",
        "ancestors": [
          "basket1",
          "basket2"
        ],
        "docFreq": 4
      },
      {
        "node": "cheese",
        "nodeScore": 2.7047482,
        "field": "product_s",
        "numDocs": 10,
        "level": 1,
        "count(*)": 1,
        "collection": "baskets",
        "ancestors": [
          "basket2"
        ],
        "docFreq": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 26
      }
    ]
  }
}

时间图表达式

上面的示例为时间图查询奠定了基础。时间图查询允许nodes函数使用时间窗口遍历图,以在时间图中显示交叉相关。nodes函数目前支持使用十秒窗口每日窗口工作日窗口进行图遍历。

十秒窗口对于日志分析中的事件关联根本原因分析非常有用。每日和工作日窗口对于关联几天后发生的事件非常有用。

为了支持时间图查询,必须在索引时间将ISO 8601 格式的截断时间戳作为字符串字段添加到日志记录中。为了支持十秒时间窗口,应将十秒截断时间戳按如下方式编入字符串字段:2021-02-10T20:51:30Z。为了支持每日和每周时间窗口,应将日截断时间戳按如下方式编入字符串字段:2021-02-10T00:00:00Z

Solr 的 Solr 日志索引工具(此处有介绍)已经添加了十秒截断时间戳。因此,那些使用 Solr 分析 Solr 日志的人可以免费获得时间图表达式。

根事件

一旦十秒窗口已编入日志记录,我们就可以设计一个查询,创建一个根事件集。我们可以使用一个使用 Solr 日志记录的示例来演示这一点。

在此示例中,我们将执行一个 Streaming Expression facet 聚合,找出平均查询时间最高的前 10 个十秒窗口。这些时间窗口可用于在时间图查询中表示慢查询事件

以下是 facet 函数

facet(solr_logs, q="+type_s:query +distrib_s:false",  buckets="time_ten_second_s", avg(qtime_i))

以下是结果片段,其中包含平均查询时间最高的 25 个窗口

{
  "result-set": {
    "docs": [
      {
        "avg(qtime_i)": 105961.38461538461,
        "time_ten_second_s": "2020-08-25T21:05:00Z"
      },
      {
        "avg(qtime_i)": 93150.16666666667,
        "time_ten_second_s": "2020-08-25T21:04:50Z"
      },
      {
        "avg(qtime_i)": 87742,
        "time_ten_second_s": "2020-08-25T21:04:40Z"
      },
      {
        "avg(qtime_i)": 72081.71929824562,
        "time_ten_second_s": "2020-08-25T21:05:20Z"
      },
      {
        "avg(qtime_i)": 62741.666666666664,
        "time_ten_second_s": "2020-08-25T12:30:20Z"
      },
      {
        "avg(qtime_i)": 56526,
        "time_ten_second_s": "2020-08-25T12:41:20Z"
      },
      ...

      {
        "avg(qtime_i)": 12893,
        "time_ten_second_s": "2020-08-25T17:28:10Z"
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 34
      }
    ]
  }
}

时间二分图

一旦我们确定了一组根事件,就可以轻松执行一个图查询,创建发生在同一十秒窗口内的日志事件类型的二分图。对于 Solr 日志,有一个名为 type_s 的字段,该字段是日志事件的类型。

为了查看在根事件的同一十秒窗口中发生了哪些日志事件,我们可以“遍历”十秒窗口并收集 type_s 字段。

nodes(solr_logs,
      facet(solr_logs,
            q="+type_s:query +distrib_s:false",
            buckets="time_ten_second_s",
            avg(qtime_i)),
      walk="time_ten_second_s->time_ten_second_s",
      gather="type_s",
      count(*))

以下是结果节点集

{

  "result-set": {
    "docs": [
      {
        "node": "query",
        "count(*)": 10,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "admin",
        "count(*)": 2,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "other",
        "count(*)": 3,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "update",
        "count(*)": 2,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "error",
        "count(*)": 1,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 50
      }
    ]
  }
}

在此结果集中,node 字段保存了与根事件在同一十秒窗口内发生的日志事件类型。请注意,事件类型包括:查询、管理、更新和错误。count(*) 显示了不同日志事件类型的度中心性。

请注意,在慢查询事件的同一十秒窗口内只有一个错误事件。

窗口参数

对于事件关联和根本原因分析,仅找到发生在相同十秒根事件窗口内的事件是不够的。需要找到发生在每个根事件之前的时间窗口内的事件。window 参数允许您将此先前的时窗指定为查询的一部分。窗口参数是一个整数,它指定在每个根事件窗口之前包含在图遍历中的十秒时间窗口数。

nodes(solr_logs,
      facet(solr_logs,
            q="+type_s:query +distrib_s:false",
            buckets="time_ten_second_s",
            avg(qtime_i)),
            walk="time_ten_second_s->time_ten_second_s",
      gather="type_s",
      window="-3",
      count(*))

请注意,此示例中的窗口参数为负数 (-3)。这将从事件中回溯时间。正窗口将向前走时间。

以下是添加窗口参数后返回的节点集。请注意,在慢查询事件之前的 3 个十秒窗口内现在有 29 个错误事件。

{
  "result-set": {
    "docs": [
      {
        "node": "query",
        "count(*)": 62,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "admin",
        "count(*)": 41,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "other",
        "count(*)": 48,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "update",
        "count(*)": 11,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "node": "error",
        "count(*)": 29,
        "collection": "solr_logs",
        "field": "type_s",
        "level": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 117
      }
    ]
  }
}

度作为关联的表示

通过对时间二部图执行链接分析,我们可以计算在指定时间窗口内发生的每种事件类型的度。我们在二部图推荐程序示例中建立了入度点积之间的直接关系。在数字信号处理领域,点积用于表示关联。在我们的时间图查询中,我们可以将入度视为根事件与在指定时间窗口内发生的事件之间的关联的表示。

滞后参数

了解关联中的滞后对于某些用例非常重要。在滞后关联中,一个事件发生,并且在延迟之后另一个事件发生。窗口参数不会捕获延迟,因为我们只知道事件发生在先前的某个窗口内。

lag 参数可用于开始计算过去十秒窗口数的窗口参数。例如,我们可以从一组根事件之前的 30 秒开始,以 20 秒窗口遍历图。通过调整滞后并重新运行查询,我们可以确定哪个滞后窗口具有最高度。由此我们可以确定延迟。

节点评分和时间异常检测

节点评分的概念可以应用于时间图查询,以查找与一组根事件相关且对根事件异常的事件。度量计算建立了事件之间的相关性,但没有建立该事件在整个图中是十分常见的事件还是特定于子图的事件。

scoreNodes 函数可以应用于基于度量和索引中节点术语的共性对节点进行评分。这将确定该事件是否对根事件异常。

scoreNodes(nodes(solr_logs,
                 facet(solr_logs,
                       q="+type_s:query +distrib_s:false",
                       buckets="time_ten_second_s",
                       avg(qtime_i)),
                 walk="time_ten_second_s->time_ten_second_s",
                 gather="type_s",
                 window="-3",
                 count(*)))

以下是应用 scoreNodes 函数后的节点集。现在我们看到得分最高的节点错误事件。此分数为我们提供了从何处开始根本原因分析的良好指示。

{
  "result-set": {
    "docs": [
      {
        "node": "other",
        "nodeScore": 23.441727,
        "field": "type_s",
        "numDocs": 4513625,
        "level": 1,
        "count(*)": 48,
        "collection": "solr_logs",
        "docFreq": 99737
      },
      {
        "node": "query",
        "nodeScore": 16.957537,
        "field": "type_s",
        "numDocs": 4513625,
        "level": 1,
        "count(*)": 62,
        "collection": "solr_logs",
        "docFreq": 449189
      },
      {
        "node": "admin",
        "nodeScore": 22.829023,
        "field": "type_s",
        "numDocs": 4513625,
        "level": 1,
        "count(*)": 41,
        "collection": "solr_logs",
        "docFreq": 96698
      },
      {
        "node": "update",
        "nodeScore": 3.9480786,
        "field": "type_s",
        "numDocs": 4513625,
        "level": 1,
        "count(*)": 11,
        "collection": "solr_logs",
        "docFreq": 3838884
      },
      {
        "node": "error",
        "nodeScore": 26.62394,
        "field": "type_s",
        "numDocs": 4513625,
        "level": 1,
        "count(*)": 29,
        "collection": "solr_logs",
        "docFreq": 27622
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 124
      }
    ]
  }
}

DAY 和 WEEKDAY 时间窗口

要切换到工作日时间窗口,我们必须首先在日志记录中使用字符串字段对 ISO 8601 时间戳进行天截断索引。在下面的示例中,字段 time_day_s 包含天截断时间戳。

然后只需在窗口参数中指定 -3DAYS。这将从默认的十秒时间窗口切换到每日窗口。

scoreNodes(nodes(solr_logs,
                 facet(solr_logs,
                       q="+type_s:query +distrib_s:false",
                       buckets="time_day_s",
                       avg(qtime_i)),
                 walk="time_day_s->time_day_s",
                 gather="type_s",
                 window="-3DAYS",
                 count(*)))

有时,您可能需要在时间向前或向后推移时跳过周末。这对于关联在工作日交易的金融工具非常有用。WEEKDAYS 时间窗口将向前或向后推移指定的工作日天数。

scoreNodes(nodes(solr_logs,
                 facet(solr_logs,
                       q="+type_s:query +distrib_s:false",
                       buckets="time_day_s",
                       avg(qtime_i)),
                 walk="time_day_s->time_day_s",
                 gather="type_s",
                 window="-3WEEKDAYS",
                 count(*)))