計算廣告實現入門-索引布林表示式

toBeTheLight發表於2023-02-23

本文重點為介紹一個計算廣告的匹配演算法,來自 Indexing Boolean Expression 。這種匹配演算法可以匹配較為複雜的布林表示式。

儘量以說人話的方式解釋這種演算法。

不涉及權重排序等規則。

概念

  • 倒排索引:Inverted Index,反向索引,根據內容查詢文件記錄

    • document1: { a: [1,2] },document2: { a:[1], b: [9] }
    • 建立倒排索引為 a.1: [document1, document2], a.2: [document1],b.1: [document2]
    • 能快速找到到內容所在的文件記錄
  • 析取:Disjunctive,邏輯或
  • 合取:Conjunctive,邏輯與,後文稱為 且
  • 析取正規化:

    • (一個或多個且)的被認為是一個 DNF
    • 如:(A)、(B) ∪ (C)、(D ∩ E) ∪ (F)
  • 合取正規化:

    • (一個或多個析取)的被認為是一個 CNF
    • 如:(A)、(B) ∩ (C)、(D ∪ E) ∩ (F)
    • 所有邏輯公式都可以轉換成合取正規化 CNF
  • 斷言:在後續的描述中,我們將最小的邏輯單元稱為一個斷言,如 (a=1 ∩ b=2) ∪ c!=3 中的,a=1、b=2、c!=3

目標

既然所有的邏輯公式都可以轉為 CNF,那麼我們的目標就是實現一個可快速查詢目標所匹配的 CNF 布林表示式(boolean expressions)的演算法。

DNF 演算法

先來匹配 DNF 表示式

  1. 首先有幾個匹配規則:

    • BE1:(a=1 且 b=2 且 c=1)
    • BE2:(a=2 且 b=3) 或 c=1
    • BE3: (a=1 且 b!=2) 或 c=1
    • BE4:(a=1 且 b!=3)
    • BE5:b!=2
  2. 有一個目標 s1,其屬性為 a=1、b=3
  3. 對所有的匹配規則中的斷言建立倒排索引

    • 索引:

      • a=1:[BE1、BE3]
      • a=2:[BE2]
      • b=2:[BE1、BE3、BE4]
      • b=3:[BE2]
      • c=1:[BE2、BE3]
      • c=2:[BE1]
    • 為什麼不建立索引 b!=2 呢?因為無法將目標的屬性轉化為非確切值進行索引命中
  4. 索引處理:

    • 使用 s1 在第 3 步的倒排索引中匹配的話我們能找到所有包含 a=1 和 b=2 的匹配規則,結果肯定是不對的,需要進一步處理
    • 或拆分:在或關係中,只需要滿足其中一部分就可能滿足整個布林表示式,所以我們將 DNF 拆分成獨立的子句 C,判讀 C 是否匹配即可
    • 且拆分:在且關係的子句中,我們將需滿足的 = 的數量記為 k
    • 將所有上述資訊計入倒排索引中
    • 將 k 為 0 的子句寫入一個特殊的 z 索引中,以避免漏掉目標無對應屬性,而無法直接命中的只有一個不等於的子句規則
  5. 倒排索引

    • a,1:

      • { C: 'BE1-C1', info: { p: '=', k: 3 }}
      • { C: 'BE3-C1', info: { p: '=', k: 1 }}
      • { C: 'BE4-C1', info: { p: '=', k: 1 }}
    • a,2:

      • { C:'BE2-C1', info: { p: '=', k: 2 }}
    • b,2:

      • { C: 'BE1-C1', info: { p: '=', k: 3 }}
      • { C: 'BE3-C1', info: { p: '!=', k: 1 }}
      • { C: 'BE4-C1', info: { p: '!=', k: 0 }}
    • b,3:

      • { C: 'BE2-C1', info: { p: '=', k: 2 }}
      • { C: 'BE4-C1', info: { p: '!=', k: 1 }}
    • c,1:

      • { C: 'BE1-C1', info: { p: '=', k: 3 }}
      • { C: 'BE2-C2', info: { p: '=', k: 1 }}
      • { C:'BE3-C2', info: { p: '=', k: 1 }
    • c,2:

      • { C: 'BE1-C2', info: { p: '=', k: 1 }}
    • z: z 非真實存在,所以我們不記錄子句資訊

      • { C:'BE5', info: { p: '=', k: 0 }}
  6. 剪枝:

    • 對於只有兩個屬性的目標,最多隻能滿足兩個等於條件
    • 所以以 s1: a=1、b=3 為例所以可以將 k > 2 的直接排除掉
    • 再進行 a,1、b,3 的查詢得出新的倒排索引
  7. 索引命中:z 預設為命中的

    • a,1:

      • { C: 'BE3-C1', info: { p: '=', k: 1 }}
      • { C: 'BE4-C1', info: { p: '=', k: 1 }}
    • b,3:

      • { C: 'BE2-C1', info: { p: '=', k: 2 }}
      • { C: 'BE4-C1', info: { p: '!=', k: 1 }}
    • z:

      • { C: 'BE5', info: { p: '=', k: 0 }}
  8. 子句判斷:以子句分組判斷子句是否匹配

    • BE2-C1:

      • 命中:{ p: '=', k: 2 }
      • 判斷:需滿足 2 個等於,子句內僅有一項,不符合
    • BE3-C1:

      • 命中:{ p: '=', k: 1 }
      • 判斷:需滿足 1 個等於,子句內有一項,符合
    • BE4-C1:

      • 命中:{ p: '=', k: 1 }、{ p: '!=', k: 1 }
      • 判斷:需滿足 1 個等於,分組內有多於一項,但是命中了一項不等於,導致子句整體判斷為假,不符合
    • BE5:

      • 命中:{ p: '=', k: 0 }
      • 判斷:需滿足 0 個(未出現不等於,這也是 z 的作用)等於,分組內為一項,符合
  9. 結果:

    1. BE5 的 第一個子句 C1 匹配
    2. true 或 ? 恆為 true
    3. 最終匹配了布林表示式 BE5
  10. 總結:

    • DNF 的 CNF 子句之間是或的關係,只需要滿足一個 CNF 子句即可
    • 子句的斷言間是且的關係,需要看是否滿足所有斷言。同時,目標屬性少於斷言數量 k 可直接排除(演算法最佳化)
    • 斷言為不等於時,需要一個特殊的 k=0 的倒排索引來命中不等於斷言

CNF

  1. 首先有幾個匹配規則:

    • BE1:(a=1 或 b=2) 且 (c=1 或 e!=2)
    • BE2:(a=2 或 b=3) 且 (c=1 或 d=2)
    • BE3: (a=2 或 b!=2) 且 (c=1 或 d!=2)
    • BE4:(a=2 或 b=3) 且 (c!=1 或 d!=2)
    • BE5:(a=1 或 e!=2)
  2. 有一個目標 s2,其屬性是:a=2、d=2
  3. 對所有的匹配規則中的斷言建立倒排索引

    • 索引:

      • a=1:[BE1、BE5]
      • a=2:[BE2、BE3、BE4]
      • b=2:[BE1、BE3]
      • b=3: [BE2、BE4]
      • c=1:[BE1、BE2、BE3、BE4]
      • d=2:[BE2、BE3、BE4]
      • e=2:[BE1、BE5]
  4. 索引處理

    • 以上索引依然不能直接匹配目標
    • 在 CNF 中,其 DNF 子句之間是且的關係,需要命中所有的 DNF 子句 C
    • DNF 的演算法我們已經知道了,可以基於 DNF 進行匹配,再進行一次合併判斷(類似於 DNF 演算法中命中的數量是否等於 k)
    • 於是需要記錄以下資訊,即 DNF 子句 C 是 CNF 的第幾部分,同時對每個子句的命中進行判斷
    • 構造一個 CNF 的真值計數器,為陣列,其長度為 DNF 子句的數量,其每項的值為表示否定的斷言的數量
    • 對於 BE3,其計數器為[-1,-1],對於 BE4,其計數器為[0,-2]
    • 同樣我們將所有子句都包含不等於的 CNF 的也計入特殊的 z 索引,以避免目標缺少屬性而無法匹配的情況
  5. 基於以上資訊我們建立:

    • 倒排索引:

      • a,1:

        • { C: 'BE1-C1', info: { p: '=' }}
        • { C: 'BE5-C1', info: { p: '=' }}
      • a,2:

        • { C: 'BE2-C1', info: { p: '=' }}
        • { C: 'BE3-C1', info: { p: '=' }}
        • { C: 'BE4-C1', info: { p: '=' }}
      • b,2:

        • { C: 'BE1-C1', info: { p: '=' }}
        • { C: 'BE3-C1', info: { p: '!=' }}
      • a,3:

        • { C: 'BE2-C1', info: { p: '=' }}
        • { C: 'BE4-C1', info: { p: '=' }}
      • c,1:

        • { C: 'BE1-C2', info: { p: '=' }}
        • { C: 'BE2-C2', info: { p: '=' }}
        • { C: 'BE3-C2', info: { p: '=' }}
        • { C: 'BE4-C2', info: { p: '!=' }}
      • d,2:

        • { C: 'BE2-C2', info: { p: '=' }}
        • { C: 'BE3-C2', info: { p: '!=' }}
        • { C: 'BE4-C2', info: { p: '!=' }}
      • e,2:

        • { C: 'BE1-C2', info: { p: '!=' }}
        • { C: 'BE5-C1', info: { p: '!=' }}
      • z:

        • { C: 'BE5', info: { p: '!=' }}
    • 真值計數器:

      • BE1:[0,-1]
      • BE2:[0,0]
      • BE3:[-1,-1]
      • BE4:[0,-2]
  6. 索引命中:s2,其屬性是:a=2、d=2

    • a,2:

      • { C: 'BE2-C1', info: { p: '=' }}
      • { C: 'BE3-C1', info: { p: '=' }}
      • { C: 'BE4-C1', info: { p: '=' }}
    • d,2:

      • { C: 'BE2-C2', info: { p: '=' }}
      • { C: 'BE3-C2', info: { p: '!=' }}
      • { C: 'BE4-C2', info: { p: '!=' }}
    • z:

      • { C: 'BE5', info: { p: '!=' }}
  7. 計數器計算:

    • BE2-C1 中命中 BE2 第 1 部分,判斷為 =,BE2 計數器,第 1 位改為 1:[1,1]
    • BE2-C2 中命中 BE2 第 2 部分,判斷為 =,BE2 計數器,第 2 位改為 1:[1,1]
    • BE3-C1 中命中 BE3 第 1 部分,判斷為 =,BE1 計數器,第 1 位改為 1:[1,-1]
    • BE3-C2 中命中 BE2 第 2 部分,判斷為 !=,BE2 計數器,第 1 位加 1:[1,0]
    • BE4-C1 中命中 BE4 第 1 部分,判斷為 =,BE2 計數器,第 2 位改為 1:[1,-2]
    • BE4-C2 中命中 BE2 第 2 部分,判斷為 !=,BE2 計數器,第 2 位加 1:[1,-1]
    • z命中,即缺屬性命中不等於判斷,即 BE5 命中
  8. 結果:

    • 計數器:

      • BE1:[0,-1]
      • BE2:[1,1]
      • BE3:[1,0]
      • BE4:[1,-1]
      • z: BE5
    • 含有 0 計數的,不匹配,所以結果為 BE2、BE4、BE5
  9. 總結:

    • CNF 為且關係的子句,需要滿足所有 DNF 子句,所以我們
    • 根據子句建立 CNF 否命中的計數器,如 BE3: (a=2 或 b!=2) 且 (c=1 或 d!=2) 真值計數器為 [-1,-1],長度為子句長度,每項為子句中 != 的數量,每有 n 個記為 -n
    • 將斷言在第幾個子句和斷言符號計入倒排索引資訊

      • 使用目標匹配所有倒排索引
      • 根據資訊判斷修改真值計數器
    • 命中第 n 個子句,且斷言符號為 =,則將真值第 n 個位置置為 1,(表示這個 DNF 子句滿足要求,或關係滿足一個即可)
    • 命中第 n 個子句,且斷言符號為 !=,則將真值第 n 個位置加 1,(表示這個 DNF 子句預設滿足要求的 != 已經失效)

範圍斷言

  • 範圍斷言:

    • 我們已經知道如何命中 = 和 !=,對於 >、< 這種範圍該怎麼辦?將值轉為確切的值即可
    • 如匹配規則:time > 2028-10

      • 轉化為 time = 2028-10、2028-11、2028-12、2029、2030、2040、2050...2100、2200...
      • 當然,不減少精度也可以,但是過多的值可能會是倒排索引數量爆炸
      • 當目標屬性 time: 2029-01 來命中時,轉化為 2029-01、2029、2020、2010、2000、1000 即可依靠 2029 命中
      • 即原始格式的值用來命中 = 斷言,小精度的值用來快速命中範圍斷言
    • 極限:我們肯定不能無限的轉化,根據實際情況對屬性轉化設定一個上下限即可

      • 年齡:上限 120、下限 0
      • 較為實時的時間屬性:在轉化和命中時取當前時間加減幾年或即可滿足需要
    • 當然也可以基於以上 CNF 的演算法自己實現範圍查詢的邏輯
  • 存在:

    • 如果對屬性是否存在也有匹配要求,即可在轉化時轉化為特殊值即可
    • 如匹配規則:a 存在 且 b 不存在

      • 轉化為:a=XXXNOTNULL、b=XXXNULL
      • 命中時,目標屬性為 a=3,則轉化為 a=3、XXXNOTNULL,b=XXXNULL 即可

實踐

以 ElasticSearch 為例,說明如何將匹配規則存入和如何匹配目標

  • 匹配規則 BE1: (a == 1 || a == 2 || b = 3) && c!=1
  • 目標:{ a: 1, b:2 }
  • 倒排索引

    • 構造 ElasticSearch 的匹配規則文件為:

      // 文件1,在查詢時 [1,2] 滿足一個即可,所以可以將同一子句中 a 的兩個值合併在一起
      {
        BE: 'BE1',
        a: [1,2],
        info: {
          C: 1,
          d: '=',
          trueList: [0, -1]
        },
      }
      // 文件2
      {
        BE: 'BE1',
        b: 3,
        info: {
          C: 1,
          d: '=',
          trueList: [0, -1]
        }
      }
      // 文件3
      {
        BE: 'BE1',
        c: 1,
        info: {
          C: 2,
          d: '!=',
          trueList: [0, -1]
        }
      }
      // 也可以將匹配規則的真值計數器 trueList 存在別的地方
    • 可以設定使得 ElasticSearch 不索引 info 和 BE 欄位
  • 構造目標查詢語句:

    {
      "query": {
        "bool": {
          "should": [
            {
              term: {
                a: 1
              }
            },
            {
              term: {
                b: 2
              }
            }
          ]
        }
      }
    }
  • 查詢結果:

    // 只命中了 a
    {
      BE: 'BE1',
      a: [1,2],
      info: {
        C: 1,
        d: '=',
        trueList: [0, -1]
      },
    }
  • 真值計算:斷言符號為 =,第一位改為[1,-1]
  • 結果:命中 BE1

參考

相關文章