Cào một mô hình dữ liệu đơn giản, tìm thấy một mô hình phức tạp

Có vẻ tôi có biệt tài phát hiện ra các trường hợp biên – hoặc trong nhiều tình huống, bản thân tôi chính là một trường hợp biên.

Một loại trong số đó là khi tôi tự thấy mình đang dùng một mô hình dữ liệu ban đầu trông có vẻ đơn giản – và rồi thế giới thực can thiệp vào. Tôi luôn thấy loại chuyện này thú vị, và gần đây tôi tình cờ gặp một ví dụ khá hay nên muốn chia sẻ. Bài này cũng cho thấy một vài quá trình tư duy mà tôi dùng khi phạm vi hóa cách tôi mô hình hóa dữ liệu.

Tôi đang cố mô hình hóa văn bản của Kinh Thánh, vì những lý do không tiện nói ở đây. BibleGateway là “nguồn sự thật” của tôi tại đây, và tất cả ảnh chụp màn hình trong bài này đều lấy từ trang đó (kèm liên kết phù hợp).

Giả định ban đầu, phạm vi và đơn giản hóa

Lưu ý: phần này không hoàn toàn đúng. Đó là cách tôi tiếp cận mô hình dữ liệu lúc ban đầu. Các phần sau sẽ cho thấy nó sụp đổ ở đâu.

Có nhiều bản dịch Kinh Thánh khác nhau. Tôi giới phạm vi mô hình của mình chỉ ở các bản dịch tiếng Anh. Tôi không quan tâm đến “bản in đầu” so với “bản in thứ hai” etc., nhưng tôi muốn có thể phân biệt giữa “New International Version” và “New International Version – UK” etc.

Mỗi bản dịch được tạo thành từ nhiều sách (từ Sáng Thế đến Khải Huyền). Mặc dù có thể tách thành “Cựu Ước” và “Tân Ước” (và một số phân loại khác, có thể), tôi không cần phân loại đó, nên nó sẽ không nằm trong mô hình. Các bản dịch khác nhau bao gồm những sách khác nhau, vì một số bản dịch có Kinh Ngụy (Apocrypha) còn một số khác thì không.

Các bản dịch khác nhau có thể gọi cùng một sách với những tiêu đề khác nhau. Ví dụ, “Song of Songs” còn được biết là “Canticle of Canticles” hay “the Song of Solomon”; tương tự “the Book of Wisdom” còn được biết là “the Wisdom of Solomon” và “Sirach” cũng còn được gọi là “Ecclesiasticus” (và một số tiêu đề khác, apparently). Tôi không đặc biệt cần biết tiêu đề nào được dùng trong từng bản dịch, nhưng tôi cần một đại diện chuẩn. (Nếu người dùng nói họ muốn xem một đoạn trong “Sirach” hay “Ecclesiasticus”, tôi muốn trả về cùng kết quả trong cả hai trường hợp, bất kể bản dịch gọi nó là gì.)

Mỗi sách được chia thành các chương, và mỗi chương được chia thành các câu. Mỗi sách bắt đầu với chương 1 và tiến triển theo cách hiển nhiên; mỗi chương bắt đầu với câu 1 và tiến triển theo cách hiển nhiên. Các bản dịch khác nhau có thể có số câu khác nhau cho cùng một chương, nhưng có cùng số chương cho cùng một sách (ngoại trừ các sách được bổ sung bằng Kinh Ngụy). Một số câu có các phần tách tùy chọn (ví dụ 25a, 25b, 25c) mà có thể không nhất quán giữa các bản dịch. Tạm thời, tôi coi các phần tách nằm ngoài phạm vi của mô hình dữ liệu (ít nhất là lúc đầu).

Văn bản trong mỗi câu có thể có một số chi tiết định dạng như thụt lề. Đôi khi có tiêu đề bên trong chương. Một số bản dịch có thể kèm tham chiếu chéo và ghi chú chú giải. Tất cả những thứ này nằm ngoài phạm vi của ít nhất bài blog này.

Nói cách khác, tôi có thể kỳ vọng một đại diện C# đơn giản sẽ trông giống như thế này:

// Note: BookId is an enum or equivalent, so that "Genesis" uses the same BookId in all translations.
public record Bible(string Id, string Description, ImmutableArray<Book> Books);

// Chapters[0] = chapter 1 etc.
public record Book(BookId Id, ImmutableArray<Chapter> Chapters);

// Verses[0] = verse 1 etc.
public record Chapter(ImmutableArray<string> Verses);

Thực tế phức tạp

Tất nhiên, bài viết này sẽ không tồn tại nếu mô hình dữ liệu thực sự đơn giản đến thế. Các khía cạnh cấp cao nhất – một Kinh Thánh có ID, mô tả và một dãy sách – thì ổn. Nhưng khi chúng ta đến phần “một sách là một dãy chương” và “một chương là một dãy câu”, hóa ra cuộc sống phức tạp hơn nhiều.

Có vẻ unlikely rằng tôi đã phát hiện ra tất cả các cách mà mô hình đơn giản bị phá vỡ, nhưng đây là những gì tôi gặp được cho đến nay.

Phạm vi câu

Không phải mọi bản dịch Kinh Thánh nào cũng cố dịch từng câu theo đúng thứ tự văn bản gốc. Ví dụ, The Message dịch theo các “mẩu” văn bản cùng lúc. Đây là đầu sách Phúc Âm John trong The Message:

John 1 trong The Message

Như bạn có thể thấy, mỗi mẩu văn bản được gắn với một phạm vi các câu (1-2, 3-5, 6-8) thay vì một câu đơn lẻ.

Điều đó đã hoàn toàn phá vỡ mô hình dữ liệu của chúng ta. Chúng ta không thể chỉ dùng một dãy câu trong chương với mỗi câu được đại diện bằng một chuỗi duy nhất. Chúng ta cần một đại diện phức tạp hơn với một kiểu chuyên dụng cho “một phần của chương”. Nó có thể trông như thế này:

public record Bible(string Id, string Description, ImmutableArray<Book> Books);
public record Book(BookId Id, ImmutableArray<Chapter> Chapters);
public record Chapter(ImmutableArray<ChapterSection> Sections);
public record ChapterSection(string Text, int StartVerse, int EndVerse);

Lưu ý rằng tại thời điểm này, nếu người dùng thực hiện tìm kiếm dựa trên sách, chương và phạm vi câu, họ có thể thấy văn bản thực sự không thuộc phạm vi đó.

Ví dụ, tìm kiếm “Genesis 1:2-7” trong The Message bắt buộc phải hiển thị either câu 1 và 8, hoặc bỏ sót các câu 2, 6 và 7, vì chúng ta không có đủ thông tin về phần nào của mỗi mẩu đến từ câu nào.

Vẫn còn, ít nhất chúng ta biết các câu theo đúng thứ tự, đúng không?

Các câu không theo thứ tự

Đôi khi, thứ tự các câu khác nhau giữa các bản dịch, ngay cả khi đánh số câu không khác. Khi hai bản dịch khác nhau về phương diện này, tự nhiên ít nhất một trong số chúng phải có các câu không theo thứ tự tự nhiên.

Điều này có thể xảy ra khắp nơi trong Kinh Thánh, nhưng ít nhất một ví dụ là ở chương 38 sách Isaia. Bản dịch New International Version (NIV) có tất cả các câu theo thứ tự tự nhiên, với các câu 21 và 22 đến sau câu 20:

Isaiah 38 trong NIV

… trong khi bản dịch Good News lại chèn chúng giữa câu 6 và 7:

Isaiah 38 trong Good News

Chúng ta vẫn có thể giữ cùng mô hình dữ liệu như trước, nhưng chúng ta cần biết rằng các câu có thể không theo thứ tự khi thực hiện tìm kiếm. Chúng ta cũng có những quyết định phải đưa ra về việc trả về gì trong các tìm kiếm.

Giả sử chúng ta tìm trong Good News Bible, một tìm kiếm “Isaiah 38:4-8” nên trả về gì? Nó có nên bao gồm các câu 21 và 22 vì chúng xuất hiện theo văn bản giữa câu 4 và 8, hay nó chỉ nên trả về các câu 4, 5, 6, 7 và 8?

Một tìm kiếm “Isaiah 38:4-21” nên trả về gì? Nhìn vào thứ tự văn bản của các câu, có lẽ chỉ nên trả về các câu 4, 5, 6 và 21 – nhưng điều đó sẽ kỳ lạ trong bất kỳ hiểu biết nào khác về phạm vi “4 đến 21”.

(Câu trả lời mà mã của chính tôi có ở đây là “4-8” thực sự có nghĩa là “chỉ các mẩu văn bản chứa bất cứ thứ gì trong phạm vi 4-8” vì vậy nó sẽ bỏ qua 21 và 22; nhưng nếu bạn tìm toàn bộ Isaiah 38, nó sẽ hiển thị tất cả các câu theo thứ tự văn bản.)

Thứ tự phụ câu

Được rồi, vì vậy không phải mọi mẩu văn bản đều là một câu duy nhất, và các câu có thể xuất hiện theo thứ tự kỳ lạ, nhưng ít nhất mỗi số câu nhất định chỉ xuất hiện một lần, đúng không? Chưa hẳn.

Trước đây tôi đã coi cách các câu bị tách (25a, 25b, 25c etc.) nằm ngoài phạm vi. Điều đó ổn gần như ở mọi nơi, nhưng chương 28 sách Sirach, ít nhất trong bản New Revised Standard Version (Anglicised) (gọi từ đây là NRSVA), kết thúc với một số cách tách câu rất kỳ lạ. Thứ tự câu là 23, 24a, 25b, 24b, 25a, 26.

Sirach 28 trong NRSVA

Tại thời điểm đó, chúng ta either phải thay đổi mô hình dữ liệu để bao gồm các phần tách, hoặc phải chấp nhận rằng sẽ có các số câu trùng lặp (23, 24, 25, 24, 25, 26). Nếu chúng ta muốn phần tách, chúng ta có thể đổi mô hình thành dạng này:

public record Bible(string Id, string Description, ImmutableArray<Book> Books);
public record Chapter(ImmutableArray<ChapterSection> Sections);
public record ChapterSection(string Text, VerseNumber StartVerse, VerseNumber EndVerse);
public record VerseNumber(int Number, char? Subverse);

(Có lẽ sẽ hợp lý hơn nếu VerseNumber là một record struct thay vì record class ngầm định, nhưng đó là chi tiết triển khai nhiều hơn.)

Thiếu câu

Một số bản dịch bỏ qua một số câu nhất định, hoặc phần của các câu. Việc chỉ bỏ qua một số văn bản xuất hiện trong một số bản chép tay khác không ảnh hưởng đến mô hình dữ liệu của chúng ta, nhưng thiếu hoàn toàn một câu thì ít nhất cũng hơi đáng ngạc nhiên. Ví dụ, lấy đầu John 5 trong NRSVA:

John 5 trong NRSVA

Câu 4 bị thiếu, mặc dù có một chú thích, nội dung là:

Các bản quyền cổ đại khác thêm, hoàn toàn hoặc một phần, waiting for the stirring of the water; 4 for an angel of the Lord went down at certain seasons into the pool, and stirred up the water; whoever stepped in first after the stirring of the water was made well from whatever disease that person had.

Điều này không yêu cầu bất kỳ thay đổi nào đối với mô hình dữ liệu, nhưng điều quan trọng là phải nhận thức được khi kiểm tra hợp lệ.

Các cách đánh số câu thay thế

Tôi không biết liệu có nhiều trường hợp như vậy không, nhưng chương 7 sách 2 Esdras có một phạm vi câu (36-105) được bao gồm trong một số phiên bản và bị bỏ qua trong các phiên bản khác – và không giống như thứ tự thay thế ở trên nơi số câu ít nhất vẫn nhất quán về văn bản được dịch, trong trường hợp này số câu cũng thay đổi. Vì vậy “câu 36” có thể chỉ vào văn bản được dịch là “The pit of torment shall appear, and opposite it shall be the place of rest; and the furnace of hell shall be disclosed, and opposite it the paradise of delight” hay “I answered and said, ‘How then do we find that first Abraham prayed for the people of Sodom, and Moses for our ancestors who sinned in the desert,”.

Bible Gateway chỉ ra các cách đánh số câu thay thế bằng chữ nghiêng:

2 Esdras 7 trong NRSVA

Chúng ta làm thế nào để đại diện điều này trong mô hình dữ liệu? Một tìm kiếm “2 Esdras 7:30-40” trả về gì? Quan trọng nhất, chúng ta sẵn sàng bỏ bao nhiêu công sức để làm cho mô hình của mình có độ trung thực cao?

Cá nhân tôi, tôi đã chọn cách “làm điều đơn giản nhất không khiến chương trình sụp đổ” – điều cuối cùng có nghĩa là tất cả các câu “thừa” được bao gồm, và cách đánh số câu dựa trên đó. Một phương án thay thế là loại bỏ tất cả các câu “thừa”, và vẫn có một hệ thống đánh số câu nhất quán duy nhất. Việc nghĩ ra một mô hình dữ liệu thực sự đại diện được cả hai cách đánh số sẽ dẫn đến rất nhiều phức tạp, và tôi chỉ làm vậy nếu tôi thực sự, thực sự cần.

Chỉ một chương

Một tìm kiếm “Obadiah 4” nên trả về gì? Đối với hầu hết các sách trong Kinh Thánh, nó sẽ trả về toàn bộ chương 4, với nhiều câu.

Tuy nhiên, không có chương 4 của Obadiah – chỉ có một chương duy nhất. Vì vậy “Obadiah 4” nên, trong một hệ thống đầy đủ về mặt chức năng, hầu như chắc chắn chỉ trả về câu 4. “Obadiah 1:4” cũng có nên được chấp nhận không? Tôi đoán điều đó tùy thuộc vào yêu cầu sản phẩm.

Đây thực chất không phải là câu hỏi về mô hình dữ liệu – nhưng nó ảnh hưởng đến cách mô hình được sử dụng, ngầm chuyển bất kỳ truy vấn nào có vẻ là “sách và chương” thành “sách, chương 1, câu” khi chỉ có một chương duy nhất.

Điều này ảnh hưởng đến chín sách trong Kinh Thánh, bao gồm Kinh Ngụy, theo những gì tôi thấy. Hầu hết đều straightforward, nhưng Thư của Jeremiah thì không. Nó chỉ có một chương – nhưng đó là chương 6. Nó không hoàn toàn kỳ lạ như âm thanh, vì nó thực chất là chương 6 của sách Baruch… nhưng được tách ra thành một sách riêng. Vì vậy trong trường hợp này, “Letter of Jeremiah 5” tương đương với “Letter of Jeremiah 6:5”. Hm.

Thiên-ca 151

Sách Thi Thiên có 150 chương. Trừ khi Kinh Thánh của bạn chứa Kinh Ngụy, khi đó Thi Thiên 151 cũng tồn tại. Nó có nên được coi là một sách riêng, hay chỉ là một chương thừa trong sách hiện có? Nó có thể được mô hình hóa theo cả hai cách, và hơi giống với Thư của Jeremiah đã nói ở trên. Dù theo cách nào, đó là điều phải được suy nghĩ cả về mặt mô hình dữ liệu lẫn cách dữ liệu sau đó được xử lý.

Các chương bắt đầu giữa câu

Hầu hết thời gian, một chương bắt đầu bằng một câu mới, và đó là bắt đầu của câu đầu tiên. Tuy nhiên, có những trường hợp có vẻ điều đó không đúng. Ví dụ, 2 Samuel 12 trong NRSVA bắt đầu “But the thing that David had done displeased the Lord, and the Lord sent Nathan to David” – nhưng cách trình bày gợi ý rằng chương 12 và câu 1 chỉ bắt đầu tại “and the Lord sent Nathan to David.” Chương nào chứa “But the thing that David had done displeased the Lord”? Đó là chương 11 câu 27, hay là văn bản không thuộc chương hay câu nào cả? Nó nên được đại diện thế nào trong mô hình dữ liệu của chúng ta?

Còn về các cách đánh số câu thay thế, câu trả lời của chính tôi là “chỉ làm cho nó đơn giản”. Tôi đã thực sự chuyển điểm bắt đầu của chương 12 câu 1 đến điểm bắt đầu của văn bản (“But the thing”). Tôi strongly nghi ngờ rằng nếu ai đó tìm “2 Samuel 12:1-4”, họ không thực sự muốn nó bắt đầu giữa câu.

Các kết thúc thay thế

Phúc Âm Mark có hai kết thúc: kết thúc “ngắn” và kết thúc “dài”.

Phiên bản kết thúc ngắn của chương 16 có 8 câu, và phiên bản kết thúc dài có 20 câu. Sẽ khá đơn giản khi có một cờ “tùy chọn” nào đó trong mô hình dữ liệu để đại diện điều đó. Tuy nhiên, trong ít nhất một số bản dịch, câu 8 trong kết thúc ngắn có văn bản bổ sung. Đây là cách nó trông trong bản dịch NRSVA của Mark 16:

Mark 16 trong NRSVA

(Kết thúc dài còn tiếp tục further.)

Đại diện đơn giản nhất là bao gồm mọi thứ từ cả hai phiên bản, không phân biệt – vì vậy bạn sẽ thấy phiên bản dài hơn của câu 8 và các câu 9-20. Tôi tin rằng đó chính xác là những gì triển khai hiện tại của tôi làm, vì một lần nữa, nó đủ tốt. Với tôi, bất kỳ đại diện “độ trung thực cao” nào cũng either phải yêu cầu người dùng chỉ ra họ muốn xem phiên bản ngắn hay phiên bản dài, hoặc hiển thị cả hai phiên bản như Bible Gateway làm, với các chú thích phù hợp.

Tôi strongly nghi ngờ còn có các sách khác có loại tính tùy chọn này nữa – mặc dù liệu chúng có có “nội dung của câu X phụ thuộc vào việc bạn có bao gồm mẩu Y hay không” thì lại là chuyện khác.

Và rồi có đến Esther Hy Lạp…

Đúng khi bạn nghĩ chúng ta đã ở cuối những điều kỳ lạ, Esther Hy Lạp xuất hiện. Mặc dù sách Esther bằng tiếng Hebrew xuất hiện trong hầu hết các bản dịch, sách Esther Hy Lạp là một phần của Kinh Ngụy. Đó là cùng một sách, nhưng với một số chương bổ sung. Trong khi Esther có 10 chương, Esther Hy Lạp có 16 chương.

Theo những gì tôi thấy, Esther Hy Lạp gần như hoàn toàn là bổ sung so với Esther – những khác biệt chỉ nằm ở văn bản bổ sung trong các chương 11-16. Ngoại lệ là chương 5, nơi các câu 1 và 2 từ phiên bản Hebrew bị bỏ qua; chương 9, nơi câu 30 bị bỏ qua; chương 10 có nhiều câu hơn ở bản Hy Lạp.

Nhưng các chương thừa không đến ở cuối. Thay vào đó, các chương được xen kẽ xuyên suốt các chương “bình thường”. Chúng không chỉ được làm như “chương 1-5, rồi chương 11” hay tương tự… một số chương bổ sung được chèn bên trong các chương gốc, và đôi khi thậm chí bị chia ra để chèn vào nhiều nơi. Chương 11 là đỉnh cao của điều này: Esther Hy Lạp bắt đầu với chương 11 câu 2, và câu cuối cùng chính là chương 11 câu 1.

Có vẻ như trong một số bản dịch, các chương được gán chữ cái thay vì số (và cũng không phải tương ứng 1:1 giữa chúng). Tôi sẽ bỏ qua phần đó, ít nhất vậy.

Nhìn vào bản dịch NRSVA, chúng ta có:

  • Chương 11, các câu 2-12 (hết)
  • Chương 12, các câu 1-6 (hết)
  • Chương 1, các câu 1-22 (hết)
  • Chương 2, các câu 1-23 (hết)
  • Chương 3, các câu 1-13
  • Chương 13, các câu 1-7
  • Chương 3, các câu 14-15 (hết)
  • Chương 4, các câu 1-17 (hết)
  • Chương 13, các câu 8-18 (hết)
  • Chương 14, các câu 1-19 (hết)
  • Chương 15, các câu 1-16 (hết)
  • Chương 5, các câu 3-14 (hết)
  • Chương 6, các câu 1-14 (hết)
  • Chương 7, các câu 1-10 (hết)
  • Chương 8, các câu 1-12
  • Chương 16, các câu 1-24
  • Chương 8, các câu 13-17 (hết)
  • Chương 9, các câu 1-29, 31-32 (hết)
  • Chương 10, các câu 1-13 (hết) (các câu 4-13 chỉ có trong Esther Hy Lạp)
  • Chương 11, câu 1

Điều này dập tắt hai giả định:

  • Các chương theo đúng thứ tự
  • Mỗi chương nhất định chỉ xuất hiện một lần

Chúng ta có thể đại diện điều này khá dễ dàng về mặt có tất cả dữ liệu đúng, bằng cách gán cho record Chapter một thuộc tính Number. Kết hợp với các bổ sung khác, chúng ta kết thúc với:

public record Bible(string Id, string Description, ImmutableArray<Book> Books);
public record Book(BookId Id, ImmutableArray<Chapter> Chapters);
public record Chapter(int Number, ImmutableArray<ChapterSection> Sections);
public record ChapterSection(string Text, VerseNumber StartVerse, VerseNumber EndVerse);
public record VerseNumber(int Number, char? Subverse);

Sau đó mọi thứ dùng dữ liệu cần biết rằng nó không thể giả định bất kỳ sự tương ứng nào giữa chỉ số của một chương trong Book.Chapters và số chương.

Kết luận

Tất cả các chi tiết ở trên có lẽ không liên quan đến bạn, trừ khi bạn tình cờ đang tạo một mô hình dữ liệu cho chính Kinh Thánh. Nhưng tôi nghĩ việc đi vào một số chi tiết cụ thể để cho thấy loại vấn đề tôi gặp phải trong gần như mọi tình huống mà thế giới thực gặp một mô hình dữ liệu lý thuyết là xứng đáng. (Trang web bầu cử là một ví dụ khác. Xử lý ngày/giờ cũng gặp quite nhiều điều này.)

Tôi ước mình có những gợi ý thật tốt về việc làm gì khi bạn gặp loại vấn đề này, nhưng điều tốt nhất tôi có thể gợi ý là dừng lại và tự hỏi mình thực sự cần gì. Tôi thấy câu trả lời thường rơi vào một trong ba nhóm:

  • Tôi có thể sống without độ trung thực cao, vì vậy không đáng làm cho mã phức tạp hơn.
  • Tôi cần các chi tiết, vì vậy tôi phải chấp nhận và sống chung với sự phức tạp.
  • Một đường ở giữa: giới thiệu một chút phức tạp hơn vào mô hình để đạt được trạng thái chấp nhận được, vốn có thể vẫn hơi kỳ lạ, nhưng tôi có thể sống được – và không liên quan đến việc làm cho mô hình dữ liệu khủng khiếp.

Nếu bạn có những điểm kỳ lạ yêu thích thuộc loại này, vui lòng để lại bình luận – luôn tốt khi sưu tầm những câu chuyện weird và wonderful.