【真的假的】2017/4 Mapping refactor ===== ## Current structure 為求專注在元件邏輯本身,author 相關 mappings(`users`、`apps`)與相關欄位外鍵(`userId`、`from`)之間的線省略。 ```graphviz graph mappings{ rankdir=LR; node[shape=record]; users [label=" 𝐮𝐬𝐞𝐫𝐬 | email\l name\l avatarUrl\l facebookId\l githubId\l twitterId\l createdAt\l updatedAt\l "] apps [label=" 𝐚𝐩𝐩𝐬 | adminUserId\l secret\l redirectUrl\l createdAt\l updatedAt\l "] replyrequests [fixedsize="true" width="2" height="1.5" label=" <id>𝐫𝐞𝐩𝐥𝐲𝐫𝐞𝐪𝐮𝐞𝐬𝐭𝐬 | <user> userId\l from\l | createdAt\l updatedAt\l "] replyconnections [fixedsize="true" width="2" height="2" label=" <id>𝐫𝐞𝐩𝐥𝐲𝐜𝐨𝐧𝐧𝐞𝐜𝐭𝐢𝐨𝐧𝐬 | <user> userId\l from\l | <reply> replyId\l | <feedbacks> feedbackIds\l | createdAt\l updatedAt\l "] replyconnectionfeedbacks [fixedsize="true" width="3" height="2" label=" <id>𝐫𝐞𝐩𝐥𝐲𝐜𝐨𝐧𝐧𝐞𝐜𝐭𝐢𝐨𝐧𝐟𝐞𝐞𝐝𝐛𝐚𝐜𝐤𝐬 | userId\l from\l | score\l comment\l createdAt\l updatedAt\l "] replies [label=" <id>𝐫𝐞𝐩𝐥𝐢𝐞𝐬 | createdAt\l | versions[].userId\l versions[].from\l | versions[].type\l versions[].text\l versions[].reference\l versions[].createdAt\l "] articles [label=" 𝐚𝐫𝐭𝐢𝐜𝐥𝐞𝐬 | <replyconnections>replyConnectionIds\l | <deletedreplyconnections>deletedReplyConnectionIds\l | <replyrequests>replyRequestIds\l | text\l createdAt\l updatedAt\l | userId\l from\l | references[].type\l references[].permalink\l references[].createdAt\l "] articles:replyrequests -- replyrequests:id [headlabel="n",taillabel="1"]; articles:replyconnections -- replyconnections:id [headlabel="n",taillabel="1"]; articles:deletedreplyconnections -- replyconnections:id [headlabel="n",taillabel="1"]; replyconnections:feedbacks -- replyconnectionfeedbacks:id [headlabel="n",taillabel="1"]; replyconnections:reply -- replies:id [headlabel="1",taillabel="n"]; } ``` ### Authentication mappings & fields 當使用者在使用 LINE bot 的時候,使用者並沒有來到 cofacts 網站進行登入。然而,LINE bot 卻能夠送出 `article` 以及 `replyrequest`。此時,`articles` 與 `replyrequests` 的作者相關欄位,應該要怎麼填寫呢? LINE bot client 本身拿得到 LINE user ID,因此可以拿 LINE 本身的 user ID 來作為判斷使用者是否為一則 article 或 replyrequest 的 identifier。但是,各個 client (LINE、web、甚至是未來會支援的第三方 client)的 user ID 都不同,因此需要 `from` 欄位。 [`from` 欄位是由 `rumors-api` 填寫的](https://github.com/cofacts/rumors-api/blob/master/src/checkHeaders.js#L4)。Client app 會送 secret 與想要存入的 `userId` 到 `rumors-api`,`rumors-api` 比對 secret 正確之後,就會把 `userId` 以及相對應的 `from` 欄位值填入。未來開放第三方 client 之後,我們可以發放 app ID & secrets(也就是目前沒有作用的 `apps` index),而 `from` 欄位可以用拿來填寫 app ID。 當 client app 存取 `rumors-api` 的 users 相關欄位的時候,[`rumors-api` 會比對 app ID 以及 userId](https://github.com/cofacts/rumors-api/blob/master/src/graphql/models/User.js#L32),如果是現在的使用者,才會回傳 users 相關欄位,避免個資外洩。 ### Design choice `articles` 對 `replyconnections` 的 1:n 關係中,`replyConnectionIds` 的外鍵之所以會存放在 `articles` 而非 `replyConnections`,主要是為了讓「列出文章時可以列出 reply 數」這個 query 可以只要查 `articles` 即可。 `replyconnections` - `replyconnectionfeedbacks`、`articles` - `replyrequests` 這兩個 1:n 關係會這樣設計也是同樣的理由,即使實際上採用了這種設計,其實無法保證 1:n 關係不會變成 m:n 關係(因為一個 replyrequests 的 ID 可以出現在複數個 articles 裡頭,所以其實不是完美的 1:n)。 ### 問題:新 filter / sorting 需求 原本用來應付 [cofacts 網頁文章列表](http://cofacts.g0v.tw) 的 index mapping 機制,無法應付下面的這些花俏的 requests——但偏偏這些 requests 非常重要。 (From: https://github.com/cofacts/rumors-db/issues/7#issuecomment-293507662 ) * 我標記成「等等回應」的文章( [#34](https://github.com/cofacts/rumors-api/issues/34) >`articles` 需新增 field `pendingRepliers` * 沒人標記成「等等回應」的所有文章( [#34](https://github.com/cofacts/rumors-api/issues/34) ) * 文章 tag ( [#32](https://github.com/cofacts/rumors-api/issues/32) ) > `articles` 需新增 field `tags` * 我回應過的 article > 列出 `replies` 中 `userId` 相符、或 `replyconections` 中 `userId` 相符的 `articles`。或許需要按照回應時間排序? * 回應中有「含有真實資訊」or「含有不實資訊」or「非文章」 > 要查找 `replies` 中,最新 version 的 type,以此來往回找 article * 回應中不含有「含有真實資訊」or「含有不實資訊」or「非文章」 * 我送出過 replyRequest 的 article (我想知道)( Related: [cofacts/rumors-site#13](https://github.com/cofacts/rumors-site/issues/13) >列出 `replyrequest` 之後再找 `articles` * 所有人都認為現有 reply 沒用的 article / 照無用度 sort (「正向」+「負向」遞增排序) > 按照 `replyconnectionfeedbacks` 的 score、group by `replyconnection` 之後抓 `articles` * 使用「各文章最近一次被回報的時間」排序 > 按照 `replyrequest` 的 `createdAt` 欄位排序 上述 filter / sort 希望可以 aggregate 在一起,例如:找出「沒人標記為『等等回應』」的文章中,回應同時有「含有真實資訊」又有「含有不實資訊」的醫療相關文章。 最後,我們也希望能在新的 schema 中放入 [segment 的設計](http://beta.hackfoldr.org/cofacts/https%253A%252F%252Fhackmd.io%252Fs%252FrJQaJ9wwl),用處是在顯示文章列表時,可以給小編更細緻的、針對個別段落的 reply 連結建議。 ## Proposed structure ```graphviz graph mappings{ rankdir=LR; node[shape=record]; replyrequests [fixedsize="true" width="2" height="1.5" label=" <id>𝐫𝐞𝐩𝐥𝐲𝐫𝐞𝐪𝐮𝐞𝐬𝐭𝐬 | <user> userId\l appId\l | <article> _parent\l | createdAt\l updatedAt\l "] replyconnections [fixedsize="true" width="2.5" height="5" label=" <id>𝐚𝐫𝐭𝐢𝐜𝐥𝐞𝐫𝐞𝐩𝐥𝐢𝐞𝐬 | <user> userId\l appId\l | <article> _parent\l | currentReply.userId\l currentReply.appId\l currentReply.type\l currentReply.text\l currentReply.reference\l currentReply.createdAt\l | <reply> replyId\l | segment\l segmentRangeStart\l segmentRangeEnd\l | status\l createdAt\l updatedAt\l "] replyconnectionfeedbacks [fixedsize="true" width="3" height="2" label=" <id>𝐚𝐫𝐭𝐢𝐜𝐥𝐞𝐫𝐞𝐩𝐥𝐲𝐟𝐞𝐞𝐝𝐛𝐚𝐜𝐤𝐬 | <replyconnection> _parent\l | userId\l appId\l | score\l comment\l createdAt\l updatedAt\l "] replies [label=" <id>𝐫𝐞𝐩𝐥𝐢𝐞𝐬 | createdAt\l | { versions\n(nested) | { userId\l appId\l | type\l text\l reference\l createdAt\l } } "] articles [label=" <id> 𝐚𝐫𝐭𝐢𝐜𝐥𝐞𝐬 | text\l createdAt\l updatedAt\l | userId\l appId\l | { references\n (nested) | { type\l permalink\l createdAt\l | userId\l appId\l } } | { pendingRepliers \n(nested) | { userId\l appid\l | createdAt\l } } | <tags>tags\l "] tags[label=" <id>𝐭𝐚𝐠𝐬 | <title>title\l | description\l | userId\l appId\l "] articles:id -- replyrequests:article [headlabel="n",taillabel="1"]; articles:id -- replyconnections:article [headlabel="n",taillabel="1"]; articles:tags -- tags:title [headlabel="m",taillabel="n"]; replyconnections:id -- replyconnectionfeedbacks:replyconnection [headlabel="n",taillabel="1"]; replyconnections:reply -- replies:id [headlabel="1",taillabel="n"]; } ``` ### Design choice * 針對 children 多、或很需要 `has_child` query 的 `replyrequest`、`replyconnectionfeedbacks` 使用 [parent/child](https://www.elastic.co/guide/en/elasticsearch/guide/current/parent-child.html) 儲存;其餘盡量使用 nested object [以求效率](https://www.elastic.co/guide/en/elasticsearch/guide/current/parent-child-performance.html)。 * `from` 欄位正名為 `appId`。author 相關 mappings(`users`、`apps`)省略,僅列出相關欄位外鍵(`userId`、`appId`)。 * `replies` 會把 cached field(`currentReply`) 塞進其所有的`replyConnection` 中,以利 aggregation 查詢。 * segments 併入 `replyconnecitons`:`replyconnecitons` 是 `articles`—`replies` n:m 關係的 join table,也就是 `articles`—`replies` 的「邊」。"segments" 是使用者圈選的字串或位置,理論上每當 `reply` 與 `article` 建立新關係時,segment 就會不同(至少字串在 `article` 上的位置會不一樣),feedbacks 也應該要重算。因此,我們選擇將 `segments` 直接實做在 `replyconnection` 上。 * 增加 `pendingReplers` (「等等回應」 [#34](https://github.com/cofacts/rumors-api/issues/34) ) * 增加 `tags` index 擺放 tag 的 metadata(例如說解釋某 tag 之類的,或是之後做 alias / redirection 消歧義之類的功能)。ID 為 `sha1(title)` 以對 tag title 做 unique constraint。 ([#32](https://github.com/cofacts/rumors-api/issues/32)),而 `artices` 增加 `tags` 欄位擺放 tags 本體。 #### 需求確認 * 沒人回應過的文章、有人回應過的文章 > 計算 child 數量,可以 ~~[針對 `has_child` query 的 constant score 做 aggregation sum](https://discuss.elastic.co/t/counting-the-number-of-children-when-returning-a-parent/9861/5)~~ 用[`has_child` query + `min_children` 參數](https://www.elastic.co/guide/en/elasticsearch/guide/current/has-child.html);使用數量做 filter 可以參考[這篇](http://stackoverflow.com/questions/34600137/elasticsearch-filtering-parents-by-filtered-child-document-count)。 * 我標記成「等等回應」的文章( [#34](https://github.com/cofacts/rumors-api/issues/34) ) > `articles.pendingRepliers` `nested` query * 沒人標記成「等等回應」的所有文章( [#34](https://github.com/cofacts/rumors-api/issues/34) ) > `articles.pendingReplies` 數量為 0 * 文章 tag ( [#32](https://github.com/cofacts/rumors-api/issues/32) ) > 直接從 article.tags 找 * 我回應過的 article > `articles` index `has_child` 特定 `currentReply.userId` * 回應中有「含有真實資訊」or「含有不實資訊」or「非文章」 > `articles` index `has_child` 特定 `currentReply.type` * 回應中不含有「含有真實資訊」or「含有不實資訊」or「非文章」 > not (`articles` index `has_child` 特定 `currentReply.type`) * 我送出過 replyRequest 的 article (我想知道)( Related: [cofacts/rumors-site#13](https://github.com/cofacts/rumors-site/issues/13) > `articles` index `has_child` 特定 `replyRequest.userId` * 照 reply 的無用度排序 (「正向」+「負向」遞增排序) > aggregate `articles/connections/replyconnectionfeedbacks` 的 `score` sum 作為排序基礎。 (https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-has-child-query.html#_scoring_capabilities) * 使用「各文章最近一次被回報的時間」排序 > 以 `/articles/replyrequest` 的 `createdAt` max 作為排序基礎 (https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-has-child-query.html#_scoring_capabilities)