2019年9月28日 星期六

Logback 輸出 JSON 格式的 log

Logback 預設通常是會輸出一行一行純文字的 log,不過如果想要輸出 JSON 格式的話,也有看起來應該是第三方貢獻給 logback 的 extension 可以使用。

Maven 設定

<properties>
	<slf4j.version>1.7.26</slf4j.version>
	<logback.version>1.2.3</logback.version>
	<logback.contrib.version>0.1.5</logback.contrib.version>
	<jackson.version>2.9.9</jackson.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>${slf4j.version}</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-core</artifactId>
		<version>${logback.version}</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback.contrib</groupId>
		<artifactId>logback-jackson</artifactId>
		<version>${logback.contrib.version}</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback.contrib</groupId>
		<artifactId>logback-json-classic</artifactId>
		<version>${logback.contrib.version}</version>
	</dependency>
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>${jackson.version}</version>
	</dependency>
</dependencies>

除了 SLF4J 和 logback 本身以外,還需要 logback-json-classic,以及因為它使用 Jackson 來做 JSON 處理,所以需要 logback-jackson 和 jackson。

logback.xml 設定

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="JSON_STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
			<layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
				<timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>
				<timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>
		
				<jsonFormatter
					class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
					<prettyPrint>true</prettyPrint>
				</jsonFormatter>
			</layout>
		</encoder>
	</appender>

	<root level="ALL">
		<appender-ref ref="JSON_STDOUT" />
	</root>
</configuration>  

這裡很單純就設定了輸出時要用 JSON,並且其中的時間格式應該要符合我設定的格式,然後要求它輸出時要 pretty print(做好縮排等格式化)。

輸出範例

{
  "timestamp" : "2019-09-28T13:04:25.013Z",
  "level" : "TRACE",
  "thread" : "main",
  "logger" : "test.App",
  "message" : "test writing log.",
  "context" : "default"
}
參考資料
  1. JSON · qos-ch/logback-contrib Wiki · GitHub
  2. How to Log in JSON with Logback

2019年9月18日 星期三

Vespa 的 keep-rank-count 和 rerank-count

在文件 [1] 上對於 keep-rank-count 的描述如下:

How many documents to keep the first phase top rank values for. Default value is 10000.

而對於 rerank-count 的描述則是如下:

Specifies the number of hits to be re-ranked in the second phase. Default value is 100. This can also be set in the query. Note that this value is local to each node involved in a query.

其實很難看懂這到底在說什麼…。

事實上,它們的功能是應該要放在一起看待的。Vespa 的 ranking 有分 first-phase 和 second-phase,其中 first-phase 會肩負減少資料量的工作,以確保當 Vespa 內存放的資料非常多的時候,不需要對所有資料都做完所有運算。keep-rank-countrerank-count 都是適用在 first-phase 階段的參數,以下分別討論它們的效果,會比較好理解。

keep-rank-count 造成的影響是當 Vespa 的每一個 search node (a.k.a. content node) 在 first-phase 取資料時,最多只會依據 rank profile 的計算去取出 keep-rank-count 指定數量的分數(和 docid),例如預設值 10,000 代表的是每個 nonde 都會暫存 10,000 筆資料的分數(和 docid),而超出這 10,000 筆資料的部份,分數就會被忽略(即只會留下 docid )。因此走到 second-phase 時就只會有 10,000 筆資料能夠被後續做更複雜的處理。這也會反應在 Vespa 最後回覆的資料中,如果是超出 keep-rank-count 的部份的資料,分數都會顯示 –infinite,代表因為分數已經在處理過程被丟棄,因此在結果中會不知道分數是多少。

rerank-count 則是決定離開 first-phase 進入到 second-phase 時,會留下多少筆資料。需要注意的是,這裡的數字一樣都是每個 search node 計算的。舉例來說,如果有 10 台 search node,rerank-count 設定為 100,那麼總共就會有 1,000 筆資料會被送進 second-phase。

keep-rank-countrerank-count 兩個結合起來,假設我的 rank profile 設定是 keep-rank-count=100, rerank-count=500,同時現在有個搜尋的指令,會搜尋到 1,000 筆資料,那麼結果會是如何呢?…..結果會是 second-phase 會看到 500 筆資料,但這 500 筆當中,只會有 100 筆的 relevance score 存在,其他 400 筆的 relevance 會顯示 -infinite

參考資料

  1. Search Definition Reference

2019年9月1日 星期日

Bloom Filter

當我們想要快速地知道某個資料是否曾經在系統中儲存過的時候,基本的作法就是時間複雜度 O(n) 或者是空間複雜度 O(n) 的選擇。

  • 時間複雜度 O(n):在存放的資料當中從第一個一直查到最後一個,花費 O(n) 的時間複雜度、同時沒有額外的空間複雜度需求,就能知道指定的某個資料到底有沒有存在。
  • 空間複雜度 O(n):存放一個配對表,例如如果資料是數字,那麼找配對表當中的指定位置,就能知道該資料是否存在。此時配對表需要花費 O(n) 的空間複雜度、但是帶來 O(1) 的時間複雜度優勢。

而 Bloom Filter 就是屬於中間型的解法,需要略高於 O(1) 的時間複雜度以及低於 O(n) 的空間複雜度。白話來說,就是 n 個資料並不需要長度為 n 的空間來存放,就能夠用來檢查 n 個資料中的任意資料是否存在於系統中。

概念上,Bloom Filter 就是使用一個陣列,用多個 hash function 來決定某個資料的位置,然後把陣列上的這些位置全都標為 1。其中資料不一定要是什麼格式,要求只是使用的 hash function 能夠輸出一個數字,來代表在陣列上的位置。因此檢查資料在不在系統中,只需要檢查這些位置是否全都是 1 就能夠知道了。不過因為是用 hash function,因此可以想見應該存在 collision 的問題,也就是有可能有多個資料會同時使用某個相同的位置,只是不會每個位置全都一樣。另外這個檢查方式也可以想像到潛在的問題:當檢查出某個資料的位置都是 1,有可能只是這些位置剛好都是被其他不同資料標上 1,並不一定真的表示我們要找的這個資料就確實存在。因此 Bloom Filter 無法保證某個資料一定存在(可能有 False Positive)、但可以保證某個資料一定不存在(不會有 False Negative)。

另一方面,因為 Bloom Filter 在儲存時只會存 0 和 1,表示它其實不知道每個位置是由哪個資料標上的。反過來說就是,當我因為加入資料的動作,把某個位置的值從 0 改為 1 後,日後我就再也無法把那個位置改回 0 了,因為 Bloom Filter 沒辦法知道這個位置跟哪個資料有關係。這也就形成 Bloom Filter 這個結構只能夠增加資料,而無法移除資料的限制。

所以簡要來說,Bloom Filter 就是個用比較少的儲存空間,快速判斷某個資料是否曾經被存放過的結構

使用情境

以 Redia 文件 [4] 的範例來說,可以用來檢查像是帳號是否被使用過:

> BF.ADD usernames funnyfred
(integer) 1
> BF.ADD usernames fredisfunny
(integer) 1
> BF.ADD usernames fred
(integer) 1
> BF.ADD usernames funfred
(integer) 1

> BF.EXISTS usernames fred
(integer) 1
> BF.EXISTS usernames fred_is_funny
(integer) 0
參考資料
  1. [論文解讀][Bloom Filter] 深入討論 Bloom Filter
  2. 資料結構大便當:Bloom Filter
  3. 海量数据处理算法—Bloom Filter
  4. Redis: Bloom Filter Pattern