ValueVector#

ValueVector 介面(在 C++ 實作中稱為 Array,並在規格中說明)是一種抽象化,用於儲存單獨欄中具有相同類型的值序列。在內部,這些值由一個或多個緩衝區表示,其數量和含義取決於向量的資料類型。

對於規格中描述的每種基本資料類型和巢狀類型,都有 ValueVector 的具體子類別。與規格中描述的類型名稱相比,在命名上有一些差異:名稱不直觀的表格(BigInt = 64 位元整數等等)。

重要的是,向量必須在嘗試讀取或寫入之前分配,ValueVector「應該」努力保證以下操作順序:建立 > 分配 > 變更 > 設定值計數 > 存取 > 清除(或分配以重新開始此過程)。我們將在下一節中透過一個具體範例來示範每個操作。

向量生命週期#

如上所述,每個向量在其生命週期中都會經歷幾個步驟,而每個步驟都由向量操作觸發。特別是,我們有以下向量操作

1. 向量建立:我們透過例如向量建構子來建立新的向量物件。以下程式碼透過建構子建立新的 IntVector

RootAllocator allocator = new RootAllocator(Long.MAX_VALUE);
...
IntVector vector = new IntVector("int vector", allocator);

到目前為止,已建立向量物件。但是,尚未分配任何底層記憶體,因此我們需要以下步驟。

2. 向量分配:在此步驟中,我們為向量分配記憶體。對於大多數向量,我們有兩個選項:1) 如果我們知道最大向量容量,我們可以透過呼叫 allocateNew(int) 方法來指定它;2) 否則,我們應該呼叫 allocateNew() 方法,並且將為其分配預設容量。對於我們的執行範例,我們假設向量容量永遠不會超過 10

vector.allocateNew(10);

3. 向量變更:現在我們可以使用我們想要的值來填充向量。對於所有向量,我們可以透過向量寫入器來填充向量值(範例將在下一節中給出)。對於基本類型,我們也可以透過 set 方法來變更向量。set 方法有兩類:1) 如果我們可以確定向量有足夠的容量,我們可以呼叫 set(index, value) 方法。2) 如果我們不確定向量容量,我們應該呼叫 setSafe(index, value) 方法,如果容量不足,它將自動處理向量重新分配。對於我們的執行範例,我們知道向量有足夠的容量,因此我們可以呼叫

vector.set(/*index*/5, /*value*/25);

4. 設定值計數:對於此步驟,我們透過呼叫 setValueCount(int) 方法來設定向量的值計數

vector.setValueCount(10);

在此步驟之後,向量進入不可變狀態。換句話說,我們不應再變更它。(除非我們透過再次分配向量來重複使用它。這將在稍後討論。)

5. 向量存取:現在是存取向量值的時候了。同樣地,我們有兩個選項可以存取值:1) get 方法和 2) 向量讀取器。向量讀取器適用於所有類型的向量,而 get 方法僅適用於基本向量。向量讀取器的具體範例將在下一節中給出。以下是透過 get 方法存取向量的範例

int value = vector.get(5);  // value == 25

6. 向量清除:當我們完成向量操作後,我們應該清除它以釋放其記憶體。這可以透過呼叫 close() 方法來完成

vector.close();

關於上述步驟的一些注意事項

  • 這些步驟不一定以線性順序執行。相反地,它們可以形成一個迴圈。例如,當向量進入存取步驟時,我們也可以回到向量變更步驟,然後設定值計數、存取向量等等。

  • 我們應該盡力確保以上步驟按順序執行。否則,向量可能處於未定義狀態,並且可能會發生一些意外行為。但是,此限制並非嚴格。這表示我們有可能違反上述順序,但仍然獲得正確的結果。

  • 當透過 set 方法變更向量值時,我們應該盡可能優先使用 set(index, value) 方法,而不是 setSafe(index, value) 方法,以避免處理向量容量的不必要的效能開銷。

  • 所有向量都實作了 AutoCloseable 介面。因此,當它們不再使用時,必須明確關閉它們,以避免資源洩漏。為了確保這一點,建議將向量相關操作放入 try-with-resources 區塊中。

  • 對於固定寬度向量(例如 IntVector),我們可以以任意順序在不同索引處設定值。但是,對於可變寬度向量(例如 VarCharVector),我們必須以索引的非遞減順序設定值。否則,設定位置之後的值將變為無效。例如,假設我們使用以下陳述式來填充可變寬度向量

VarCharVector vector = new VarCharVector("vector", allocator);
vector.allocateNew();
vector.setSafe(0, "zero");
vector.setSafe(1, "one");
...
vector.setSafe(9, "nine");

然後我們再次設定位置 5 的值

vector.setSafe(5, "5");

在那之後,向量位置 6、7、8 和 9 的值將變為無效。

建構 ValueVector#

請注意,目前的實作不會強制執行 Arrow 物件是不可變的規則。ValueVector 實例可以直接使用 new 關鍵字建立,有 set/setSafe API 和 FieldWriter 的具體子類別用於填充值。

例如,以下程式碼顯示如何建構 BigIntVector,在本例中,我們建構一個範圍從 0 到 7 的向量,其中應該保存第四個值的元素為空值

try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE);
  BigIntVector vector = new BigIntVector("vector", allocator)) {
  vector.allocateNew(8);
  vector.set(0, 1);
  vector.set(1, 2);
  vector.set(2, 3);
  vector.setNull(3);
  vector.set(4, 5);
  vector.set(5, 6);
  vector.set(6, 7);
  vector.set(7, 8);
  vector.setValueCount(8); // this will finalizes the vector by convention.
  ...
}

BigIntVector 保留兩個 ArrowBufs。第一個緩衝區保存空值位元圖,此處由單個位元組組成,位元為 1|1|1|1|0|1|1|1(如果值為非空值,則位元為 1)。第二個緩衝區包含以上所有值。由於第四個條目為空值,因此該位置在緩衝區中的值未定義。請注意,與 set API 相比,setSafe API 將在設定值之前檢查值容量,並在必要時重新分配緩衝區。

以下是如何使用 writer 建構向量

try (BigIntVector vector = new BigIntVector("vector", allocator);
  BigIntWriter writer = new BigIntWriterImpl(vector)) {
  writer.setPosition(0);
  writer.writeBigInt(1);
  writer.setPosition(1);
  writer.writeBigInt(2);
  writer.setPosition(2);
  writer.writeBigInt(3);
  // writer.setPosition(3) is not called which means the fourth value is null.
  writer.setPosition(4);
  writer.writeBigInt(5);
  writer.setPosition(5);
  writer.writeBigInt(6);
  writer.setPosition(6);
  writer.writeBigInt(7);
  writer.setPosition(7);
  writer.writeBigInt(8);
}

有 get API 和 FieldReader 的具體子類別用於存取向量值,需要聲明的是 writer/reader 不如直接存取有效率

// access via get API
for (int i = 0; i < vector.getValueCount(); i++) {
  if (!vector.isNull(i)) {
    System.out.println(vector.get(i));
  }
}

// access via reader
BigIntReader reader = vector.getReader();
for (int i = 0; i < vector.getValueCount(); i++) {
  reader.setPosition(i);
  if (reader.isSet()) {
    System.out.println(reader.readLong());
  }
}

建構 ListVector#

ListVector 是一個向量,它為每個索引保存一個值列表。使用它時,您需要處理與上述相同的步驟(建立 > 分配 > 變更 > 設定值計數 > 存取 > 清除),但是您完成此操作的細節略有不同,因為您需要同時建立向量並為每個索引設定值列表。

例如,以下程式碼顯示如何使用 writer UnionListWriter 建構 int 的 ListVector。我們建構一個從 0 到 9 的向量,並且每個索引都包含一個列表,其值為 [[0, 0, 0, 0, 0], [0, 1, 2, 3, 4], [0, 2, 4, 6, 8], …, [0, 9, 18, 27, 36]]。列表值可以以任何順序新增,因此寫入諸如 [3, 1, 2] 之類的列表也同樣有效。

try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE);
  ListVector listVector = ListVector.empty("vector", allocator)) {
  UnionListWriter writer = listVector.getWriter();
  for (int i = 0; i < 10; i++) {
     writer.startList();
     writer.setPosition(i);
     for (int j = 0; j < 5; j++) {
         writer.writeInt(j * i);
     }
     writer.setValueCount(5);
     writer.endList();
  }
  listVector.setValueCount(10);
}

ListVector 值可以透過 get API 或透過 reader 類別 UnionListReader 存取。若要讀取所有值,請先列舉索引,然後列舉內部列表值。

// access via get API
for (int i = 0; i < listVector.getValueCount(); i++) {
   if (!listVector.isNull(i)) {
       ArrayList<Integer> elements = (ArrayList<Integer>) listVector.getObject(i);
       for (Integer element : elements) {
           System.out.println(element);
       }
   }
}

// access via reader
UnionListReader reader = listVector.getReader();
for (int i = 0; i < listVector.getValueCount(); i++) {
   reader.setPosition(i);
   while (reader.next()) {
       IntReader intReader = reader.reader();
       if (intReader.isSet()) {
           System.out.println(intReader.readInteger());
       }
   }
}

字典編碼#

字典編碼是一種壓縮形式,其中一種類型的值被較小類型的值取代:整數陣列取代字串陣列是一個常見的範例。原始值和取代值之間的對應關係保存在「字典」中。由於字典只需要每個較長值的副本,因此字典和較小值陣列的組合可能會使用更少的記憶體。原始資料的重複性越高,節省的空間就越多。

可以對 FieldVector 進行字典編碼,以提高效能或改善記憶體效率。如果有很多值,但唯一值很少,則幾乎可以編碼任何類型的向量。

編碼過程涉及幾個步驟

  1. 建立常規的未編碼向量並填充它

  2. 建立與未編碼向量類型相同的字典向量。此向量必須具有相同的值,但未編碼向量中的每個唯一值在此處只需要出現一次。

  3. 建立 Dictionary。它將包含字典向量,以及一個 DictionaryEncoding 物件,該物件保存編碼的中繼資料和設定值。

  4. 建立 DictionaryEncoder

  5. DictionaryEncoder 上呼叫 encode() 方法以產生原始向量的編碼版本。

  6. (可選)在編碼向量上呼叫 decode() 方法以重新建立原始值。

編碼值將是整數。根據您擁有的唯一值數量,您可以使用 TinyIntVectorSmallIntVectorIntVectorBigIntVector 來保存它們。當您建立 DictionaryEncoding 實例時,您可以指定類型。您可能想知道這些整數從何而來:字典向量是一個常規向量,因此該值在該向量中的索引位置用作其編碼值。

DictionaryEncoding 中的另一個關鍵屬性是 id。務必了解 id 的使用方式,因此我們將在本節稍後介紹。

此結果將是一個新向量(例如,IntVector),它可以代替原始向量(例如,VarCharVector)運作。當您以 arrow 格式寫入資料時,寫入的是新的 IntVector 加上字典:您稍後將需要字典來檢索原始值。

// 1. create a vector for the un-encoded data and populate it
VarCharVector unencoded = new VarCharVector("unencoded", allocator);
// now put some data in it before continuing

// 2. create a vector to hold the dictionary and populate it
VarCharVector dictionaryVector = new VarCharVector("dictionary", allocator);

// 3. create a dictionary object
Dictionary dictionary = new Dictionary(dictionaryVector, new DictionaryEncoding(1L, false, null));

// 4. create a dictionary encoder
DictionaryEncoder encoder = new DictionaryEncoder.encode(dictionary, allocator);

// 5. encode the data
IntVector encoded = (IntVector) encoder.encode(unencoded);

// 6. re-create an un-encoded version from the encoded vector
VarCharVector decoded = (VarCharVector) encoder.decode(encoded);

我們尚未討論的一件事是如何從原始未編碼值建立字典向量。這留給程式庫使用者,因為自訂方法可能比通用實用程式更有效率。由於字典向量只是常規向量,因此您可以使用標準 API 填充其值。

最後,您可以將多個字典打包在一起,如果您正在使用具有多個字典編碼向量的 VectorSchemaRoot,這會很有用。這是使用名為 DictionaryProvider 的物件完成的。如下面的範例所示。請注意,我們不會將字典向量放在與資料向量相同的 VectorSchemaRoot 中,因為它們通常具有較少的值。

DictionaryProvider.MapDictionaryProvider provider =
    new DictionaryProvider.MapDictionaryProvider();

provider.put(dictionary);

DictionaryProvider 只是識別碼到 Dictionary 物件的對應,其中每個識別碼都是一個長值。在上面的程式碼中,您會看到它作為 DictionaryEncoding 建構子的第一個引數。

這就是 DictionaryEncoding 的「id」屬性的作用。此值用於使用 DictionaryProvider 將字典連接到 VectorSchemaRoot 的實例。以下是其運作方式

  • VectorSchemaRoot 具有包含 Field 物件列表的 Schema 物件。

  • 欄位具有一個名為「dictionary」的屬性,但它保存的是 DictionaryEncoding 而不是 Dictionary

  • 如前所述,DictionaryProvider 保存由長值索引的字典。此值是來自您的 DictionaryEncoding 的 id。

  • 若要檢索 VectorSchemaRoot 中向量的字典,您可以取得與向量關聯的欄位,取得其字典屬性,並使用該物件的 id 在提供者中查閱正確的字典。

// create the encoded vector, the Dictionary and DictionaryProvider as discussed above

// Create a VectorSchemaRoot with one encoded vector
VectorSchemaRoot vsr = new VectorSchemaRoot(List.of(encoded));

// now we want to decode our vector, so we retrieve its dictionary from the provider
Field f = vsr.getField(encoded.getName());
DictionaryEncoding encoding = f.getDictionary();
Dictionary dictionary = provider.lookup(encoding.getId());

如您所見,DictionaryProvider 對於管理與 VectorSchemaRoot 關聯的字典非常方便。更重要的是,它有助於在寫入 VectorSchemaRoot 時打包字典。ArrowFileWriterArrowStreamWriter 類別都接受用於該目的的可選 DictionaryProvider 引數。您可以在 (讀取/寫入 IPC 格式) 的文件中找到用於寫入字典的範例程式碼。ArrowReader 及其子類別也實作了 DictionaryProvider 介面,因此您可以在讀取檔案時檢索實際的字典。

切片#

與 C++ 實作類似,可以對向量進行零複製切片,以透過 TransferPair 取得參考資料的某些邏輯子序列的向量

IntVector vector = new IntVector("intVector", allocator);
for (int i = 0; i < 10; i++) {
  vector.setSafe(i, i);
}
vector.setValueCount(10);

TransferPair tp = vector.getTransferPair(allocator);
tp.splitAndTransfer(0, 5);
IntVector sliced = (IntVector) tp.getTo();
// In this case, the vector values are [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] and the sliceVector values are [0, 1, 2, 3, 4].