<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gyula Lakatos</title>
    <description>The latest articles on DEV Community by Gyula Lakatos (@laxika).</description>
    <link>https://dev.to/laxika</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F990164%2Fa0f1d61a-51a2-40ef-923f-c7dc50cd8578.png</url>
      <title>DEV Community: Gyula Lakatos</title>
      <link>https://dev.to/laxika</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/laxika"/>
    <language>en</language>
    <item>
      <title>Tanais Online, Week 3 - 4.</title>
      <dc:creator>Gyula Lakatos</dc:creator>
      <pubDate>Mon, 21 Oct 2024 19:48:55 +0000</pubDate>
      <link>https://dev.to/laxika/tanais-online-week-3-4-34a8</link>
      <guid>https://dev.to/laxika/tanais-online-week-3-4-34a8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hi dear readers!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the second entry in my blog about the game I'm working on. Let's look into what was added/changed in the past two weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first thing I added was more cities. This was quite trivial after I did the basic historical research.&lt;/p&gt;

&lt;p&gt;The second one was adding city &lt;strong&gt;populations&lt;/strong&gt;. I wanted to control how many units can players recruit and the best thing to do that was capping it by population.&lt;/p&gt;

&lt;p&gt;These were the plans for population originally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Represents the population of the city. It is controlled by the birth rate and death rate mechanics. &lt;del&gt;Initially,&lt;/del&gt; each settlement has a higher birth rate than the death rate, &lt;del&gt;but this can be influenced by squalor and diseases&lt;/del&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;del&gt;If squalor is low then the birth rate increases, if it is high, then the birth rate decreases. When there is a disease outbreak, the death rate grows significantly.&lt;/del&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;del&gt;Population is required to raise armies/units and provides&lt;/del&gt; one gold income for each population once each real-life day."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I added both the death rate and the birth rate mechanisms. Based on historical sources, the birth rate was 40 people per 1000 people per year. The death rate was 38, but I set it to 30 instead to give way to wars (that should ideally contribute an extra 8 persons 🤔).&lt;/p&gt;

&lt;p&gt;Also, each person gives 1 gold on every real-life day so people will think twice about raising armies (which might provide plenty of income in case of a victory, but not much if the armies stand around doing nothing or even worse keep losing).&lt;/p&gt;

&lt;p&gt;Gold was the first resource I had to add, so I needed to create a "resource system" as well, which is at the moment just linking the resources to the nations in the game. Each nation has a field for each resource and when the game refreshes, the nation's money is recalculated based on the population (added together) the nation has.&lt;/p&gt;

&lt;p&gt;I added districts as well with the base of buildings:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Building a district costs 500 gold and takes 8 real-life hours.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;There are five districts in the game: Industrial, Military, Cultural (incl. Religious), Entertainment, and Agricultural.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Villages can have two districts, small cities can have three, large cities can have four and metropolises can have five (city level is based on population).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also had to add an event system to support the building times. The application updates every game once every 30 seconds.&lt;/p&gt;

&lt;p&gt;This was the time I decided that &lt;strong&gt;enough is enough&lt;/strong&gt;! Saving everything to the database as &lt;del&gt;it should be&lt;/del&gt; expected to be in a relation-based DB was very cumbersome. I decided that it would be better if I saved the whole game state (except a few variables) as JSON. This makes the saving/loading a lot easier and I wouldn't search for games based on the game state anyway.&lt;/p&gt;

&lt;p&gt;Now, the game table looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;changeSet&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;author=&lt;/span&gt;&lt;span class="s"&gt;"laxika"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"game"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt; &lt;span class="na"&gt;autoIncrement=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;constraints&lt;/span&gt; &lt;span class="na"&gt;primaryKey=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"varchar(16)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"gameState"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"json"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;createIndex&lt;/span&gt; &lt;span class="na"&gt;indexName=&lt;/span&gt;&lt;span class="s"&gt;"ix_status"&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"game"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/createIndex&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/changeSet&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The settlement, nation, and district tables are no longer there. I synchronize on the game instance every time I change something (to make changes atomic) and save the game data every 10 turns (5 minutes).&lt;/p&gt;

&lt;p&gt;Also, there were some UI changes as well, but the design is still very wireframe-like 🙂.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaw081o0do2f0o9gr0xf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaw081o0do2f0o9gr0xf.png" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Tanais Online, Week 1 - 2.</title>
      <dc:creator>Gyula Lakatos</dc:creator>
      <pubDate>Mon, 07 Oct 2024 12:24:09 +0000</pubDate>
      <link>https://dev.to/laxika/tanais-online-week-1-2-3ebm</link>
      <guid>https://dev.to/laxika/tanais-online-week-1-2-3ebm</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hi dear readers!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This entry is the first in a playful bi-weekly series about developing a game called Tanais Online. Don't expect it to be very detailed. I'm a lone dev who has minimal time and super high ambitions so the focus is on devving. If you have any questions feel free to ask in the comments.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First of all, what is Tanais Online? It's a browser game that aims to be a cross between &lt;a href="https://www.youtube.com/watch?v=Lnf45OeeHQo" rel="noopener noreferrer"&gt;Total War: Attila&lt;/a&gt; — the last good historical TW game (yes, feel free to go at me in the comments, idc 😝) — and &lt;a href="https://www.supremacy1914.com/" rel="noopener noreferrer"&gt;Supremacy 1914&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I played the former an awful lot (1303,6 hours to be exact) and found it awesome. Classical Antiquity rocks in general. Idk why there are no more games set in that age. If nobody creates them then it should be a good market for new releases, right? RIGHT? I hope so hah. We will see. In the worst case, I will create a game that I can play for the rest of my life, even if CA keeps failing with all its historical releases forever.&lt;/p&gt;

&lt;p&gt;I started two weeks ago by sitting down to plan my game in &lt;a href="https://milanote.com/" rel="noopener noreferrer"&gt;Milanote&lt;/a&gt;. They had an ad on YT and it looked like the best tool for the job. I quickly added a gazillion small notes and hit the free limit. I realized the unlimited plan cost $10 a month. Whhaaaat? So I just did some Sulla-like purges between those ideas hopefully keeping the most successful ones.&lt;/p&gt;

&lt;p&gt;I exported &amp;amp; uploaded the full plan to &lt;a href="https://drive.usercontent.google.com/download?id=1ZA9YTkJembgw8WKv0U6-gCLzHx3VQae2" rel="noopener noreferrer"&gt;here&lt;/a&gt;. You will see excerpts from it all around in the next episodes.&lt;/p&gt;

&lt;p&gt;After I got a plan I started to work on the hardest part, the graphical representation. I have the advantage that I'm working on a web game, so if I get lucky, I can just render everything in the browser. What a brilliant idea! 😅 After some thinking and research I decided that my target should be something like what's present &lt;a href="https://www.totalwar.com/blog/thrones-campaign-map-reveal/" rel="noopener noreferrer"&gt;here&lt;/a&gt; (check the last image). If I can get something like that and I can show some armies marching on it I will be 98% done (aka almost). After some Gimping, headaches, and a few laughs I ended up with this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8wh7uxu2mf28m8c2veq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8wh7uxu2mf28m8c2veq.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I know it looks terrible, but honestly, it still looks better than I expected. If I buy David Baumgart's &lt;a href="https://cubebrush.co/dgbaumgart?product_id=aueq1a" rel="noopener noreferrer"&gt;terrain pack&lt;/a&gt;, it will look okay!&lt;/p&gt;

&lt;p&gt;Other than the map UI, I started working on the backend code as well. Login &amp;amp; registration is done with the home screen that shows the running and soon starting games.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fezh6uen5b14wlxafuxkz.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fezh6uen5b14wlxafuxkz.PNG" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was harder than I initially thought it to be. Adding websocket support &amp;amp; MySQL-based saving was easy, but coming up with the game's initializing logic was not.&lt;/p&gt;

&lt;p&gt;I ended up creating 4 tables. The player table stores the users, the game table stores the games, the nations table stores the nations in those games, and the settlement table stores the settlements owned by those nations. Maybe using document storage (Mongo?) would have been easier, but I wanted to have "proper", easy-to-use (doesn't exist anywhere but ok) transaction support.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;changeSet&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;author=&lt;/span&gt;&lt;span class="s"&gt;"laxika"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"game"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt; &lt;span class="na"&gt;autoIncrement=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;constraints&lt;/span&gt; &lt;span class="na"&gt;primaryKey=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"scenarioId"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"int"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"varchar(16)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"startTime"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"lastUpdated"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"nation"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt; &lt;span class="na"&gt;autoIncrement=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;constraints&lt;/span&gt; &lt;span class="na"&gt;primaryKey=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"gameId"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ownedBy"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"varchar(16)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ownerId"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"nation"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"varchar(32)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"settlement"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt; &lt;span class="na"&gt;autoIncrement=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;constraints&lt;/span&gt; &lt;span class="na"&gt;primaryKey=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"gameId"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"settlementInScenarioId"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"int"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ownerId"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/changeSet&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in the code, I have scenarios (actually just one) that are blueprints for games. There is an update logic that runs every 30 seconds, update every game and if there are less than five games under preparation it creates as many as needed to match five. The nations and settlements are spawned at game initialization as they are in the scenario. Then once a player joins, it overtakes an AI's place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixedRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;scheduleGameRefresh&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//TODO: Create as many as needed to match 5 :)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activeGameContainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPreparingGames&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Less than 5 active games. Spawning a new one."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newGame&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;activeGameContainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerGame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;activeGameContainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActiveGames&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Updating game {}."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all for now. In the next two weeks, I plan on adding the logic for actually starting a game, showing cities and nations to the in-game players, and &lt;em&gt;maybe&lt;/em&gt; resource generation.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>How I archived 100 million PDF documents... - Part 3: Deduplication &amp; Compression</title>
      <dc:creator>Gyula Lakatos</dc:creator>
      <pubDate>Mon, 06 Feb 2023 15:52:13 +0000</pubDate>
      <link>https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-3-1cih</link>
      <guid>https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-3-1cih</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How I archived 100 million PDF documents..." is a series about my experiences with data collecting and archival while working on the &lt;a href="https://github.com/bottomless-archive-project/library-of-alexandria" rel="noopener noreferrer"&gt;Library of Alexandria&lt;/a&gt; project. My local instance just hit 100 million documents. It's a good time to pop a 🍾 and remember how I got here.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The 1-st part of the series (Reasons &amp;amp; Beginning) is available &lt;a href="https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-1-1ha7"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;The 2-nd part of the series (Indexing, Search &amp;amp; UI) is available &lt;a href="https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-2-ffg"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The previous article discussed why I started using SQL to help with the communication between the newly split applications. Also, indexing and a basic search UI were added to the applications as well, so the users can easily browse the downloaded documents.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Saving space
&lt;/h2&gt;

&lt;p&gt;Soon after I got the first couple of million documents I realized that I'll need some space to store them. A LOT of space, actually. Because of this realization, I started to look for ideas that could save me as much space as possible, so I can store as many documents as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplication
&lt;/h2&gt;

&lt;p&gt;While searching using the web UI, I found a couple of duplicates in the dataset. This was not too problematic in the beginning, but more documents the program downloaded, the more duplicates I saw on the frontend. I had to do something.&lt;/p&gt;

&lt;p&gt;My first idea was to use a &lt;a href="https://en.wikipedia.org/wiki/Bloom_filter" rel="noopener noreferrer"&gt;Bloom filter&lt;/a&gt;. Initially, it felt like a good idea. I initialized a filter with an expectation of 50 million items and a 1% false positive probability. Like what fool would collect more than 50 million documents? Guess what, I ended up throwing the whole thing into the garbage bin after hitting 5 million files in a couple of weeks. Who would want to re-size the Bloom filter every time after a new max value is hit? Also, 1% of false positives felt way too high.&lt;/p&gt;

&lt;p&gt;The next try was to calculate file &lt;a href="https://en.wikipedia.org/wiki/Checksum" rel="noopener noreferrer"&gt;checksums&lt;/a&gt;. A checksum is useful to verify file integrity after long time of storage, but it can also be used to detect duplicates. I started with &lt;a href="https://en.wikipedia.org/wiki/MD5" rel="noopener noreferrer"&gt;MD5&lt;/a&gt; as a hash function to generate the checksums. It is well known that albeit MD5 is super quick, it is broken for password hashing. Still I thought that it can work for files nevertheless. Unfortunately, there is a thing that's called &lt;a href="https://en.wikipedia.org/wiki/Hash_collision" rel="noopener noreferrer"&gt;hash-collision&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After learning that MD5 can have collisions, especially if we take the &lt;a href="https://en.wikipedia.org/wiki/Birthday_problem" rel="noopener noreferrer"&gt;Birthday problem&lt;/a&gt; into consideration, I wanted something better. This is when I realized that by using &lt;a href="https://en.wikipedia.org/wiki/SHA-2" rel="noopener noreferrer"&gt;SHA-256&lt;/a&gt;, the chance of a collision was significantly lower. Luckily, my code was quite well abstracted, so it was easy to replace the MD5 generation with SHA-256. In the final duplicate detection algorithm, a document can be considered a duplicate if its &lt;b&gt;file type (extension)&lt;/b&gt;, &lt;b&gt;file size&lt;/b&gt;, and &lt;b&gt;checksum&lt;/b&gt; is the same. After implementing the change, I had to re-crawl all the documents, but finally without duplication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uhikvvsd7qj7vckagug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uhikvvsd7qj7vckagug.png" alt="Hash collision probabilities" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;small&gt;
Hash collision probabilities.&lt;/small&gt;
&lt;br&gt;
&lt;small&gt;The lighter fields in this table show the number of hashes needed to achieve the given probability of collision (column) given a hash space of a certain size in bits (row). Using the birthday analogy: the "hash space size" resembles the "available days", the "probability of collision" resembles the "probability of shared birthday", and the "required number of hashed elements" resembles the "required number of people in a group".&lt;/small&gt;
&lt;br&gt;
&lt;small&gt;© &lt;a href="https://en.wikipedia.org/wiki/Birthday_problem" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt; - &lt;a href="https://creativecommons.org/licenses/by-sa/3.0/deed.en_US" rel="noopener noreferrer"&gt;CC BY-SA 3.0&lt;/a&gt;&lt;/small&gt;



&lt;h2&gt;
  
  
  Compression
&lt;/h2&gt;

&lt;p&gt;Removing duplicates saved a lot of space, but still, I kept acquiring more documents than what I had space for. This made me really desperate to lower the average document size, so I came up with an easy idea to cram more documents into the same space. I planned to compress them.&lt;/p&gt;

&lt;p&gt;There are a couple of ways to compress a PDF document. Or rather a couple of &lt;a href="https://en.wikipedia.org/wiki/Lossless_compression" rel="noopener noreferrer"&gt;lossless&lt;/a&gt; compression algorithms to be correct.&lt;/p&gt;

&lt;p&gt;The first thing I looked into was the good old &lt;a href="https://en.wikipedia.org/wiki/Deflate" rel="noopener noreferrer"&gt;Deflate&lt;/a&gt; algorithm with &lt;a href="https://en.wikipedia.org/wiki/Gzip" rel="noopener noreferrer"&gt;GZIP&lt;/a&gt; as the file format. It had certain advantages. First of all, it was very &lt;b&gt;mature&lt;/b&gt; supported by almost anything, including native Java (albeit later on, I switched to the &lt;a href="https://commons.apache.org/proper/commons-compress/" rel="noopener noreferrer"&gt;Apache Compress&lt;/a&gt; library for usability reasons). Secondly, it was very fast and had an "okay" compression ratio.&lt;/p&gt;

&lt;p&gt;GZIP was good enough for most of the time, but when I had spare CPU cycles to use, I wanted to re-compress the documents with something that had a better compression ratio. This was the time when I found out about the &lt;a href="https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Markov_chain_algorithm" rel="noopener noreferrer"&gt;LZMA&lt;/a&gt; encoding. Unlike GZIP (which uses a combination of &lt;a href="https://en.wikipedia.org/wiki/LZ77_and_LZ78#LZ77" rel="noopener noreferrer"&gt;Z77&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Huffman_coding" rel="noopener noreferrer"&gt;Huffman coding&lt;/a&gt;), it does &lt;a href="https://en.wikipedia.org/wiki/Dictionary_coder" rel="noopener noreferrer"&gt;dictionary based&lt;/a&gt; compression. It is a &lt;b&gt;mostly mature&lt;/b&gt; algorithm too, that has an excellent compression ratio and good decompression speed but abysmal compression speed. Ideal for long-term archival, especially when paired with an extensive amount of free CPU resources.&lt;/p&gt;

&lt;p&gt;The final candidate for compression was &lt;a href="https://en.wikipedia.org/wiki/Brotli" rel="noopener noreferrer"&gt;Brotli&lt;/a&gt;, a relative new algorithm that was originally intended to replace deflate on the world wide web. It is mainly used by &lt;a href="https://en.wikipedia.org/wiki/Web_server" rel="noopener noreferrer"&gt;web servers&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Content_delivery_network" rel="noopener noreferrer"&gt;content delivery networks&lt;/a&gt; to serve web data. Unfortunately, I found just one library that supported it in Java (&lt;a href="https://github.com/hyperxpro/Brotli4j" rel="noopener noreferrer"&gt;Brotli4J&lt;/a&gt;) and even that one was not a real Java rewrite but a wrapper around the &lt;a href="https://github.com/google/brotli" rel="noopener noreferrer"&gt;native library&lt;/a&gt; provided by Google. It's feels &lt;b&gt;very immature&lt;/b&gt;, mostly because it was released in 2015 (unlike Deflate which was released in 1951 and LZMA in 1998). But, it provides the best compression ratio out of the tree by far. Unfortunately, the resource usage is very high as well, it is the slowest one on the list. ALso, to function, it requires a native port for each and every operation system. A hustle to deal with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part four will describe how more challenges around the database scalability (replacing MySQL) and the application decoupling will be solved.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>devjournal</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>How I archived 100 million PDF documents... - Part 2: Indexing, Search &amp; UI</title>
      <dc:creator>Gyula Lakatos</dc:creator>
      <pubDate>Wed, 18 Jan 2023 14:14:25 +0000</pubDate>
      <link>https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-2-ffg</link>
      <guid>https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-2-ffg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How I archived 100 million PDF documents..." is a series about my experiences with data collecting and archival while working on the &lt;a href="https://github.com/bottomless-archive-project/library-of-alexandria" rel="noopener noreferrer"&gt;Library of Alexandria&lt;/a&gt; project. My local instance just hit 100 million documents. It's a good time to pop a 🍾 and remember how I got here.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The 1-st part of the series (Reasons &amp;amp; Beginning) is available &lt;a href="https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-1-1ha7"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The previous article dicussed why I started the project, how the URL collection was done using Common Crawl, and the ways the documents were verified if they are correct or not. In the end, we got an application that was able to collect 10.000 correct PDF documents that can be opened with a PDF viewer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Okay, now what?
&lt;/h2&gt;

&lt;p&gt;While manually &lt;a href="https://en.wikipedia.org/wiki/Smoke_testing_(software)" rel="noopener noreferrer"&gt;smoke-testing&lt;/a&gt; the application, I quickly realized that I get suboptimal download speed because the documents were processed one-by-one on just one thread. It was time to &lt;a href="https://en.wikipedia.org/wiki/Parallel_computing" rel="noopener noreferrer"&gt;parallelize&lt;/a&gt; our document handling logic, but to do that first it was needed to synchronize the URL link generation with the downloading of documents. It makes no sense to generate 10.000 URLs per second in the memory while we can only visit 10 locations per second. We will just fill our memory with a bunch of URLs and get an OutOfMemory error pretty quickly. It was time to split up our application and introduce a datastore that can act as an intermediate meeting place between the two applications. Let me introduce &lt;a href="https://www.mysql.com/" rel="noopener noreferrer"&gt;MySQL&lt;/a&gt; to you all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshk0hhdu6fyb7vsjtxuc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshk0hhdu6fyb7vsjtxuc.jpg" alt="Image description" width="600" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Was splitting up the application a good idea? Absolutely! What about introducing MySQL? You can make a guess right now. What do you think, how can MySQL handle a couple of hundred million strings in one table? Let me help you. "Super badly" is an understatement compared to how awful the performance ended up long term. But I didn't know that at the time, so let's proceed with the integration of the said database. After the app was split into two, and the newly created "document location generator" application saved the URLs into a table (with a flag that can determine if the location was visited or not) the downloader application was able to visit them. Guess what? When I ran the whole app overnight, I got hundreds of thousands of documents saved next morning (my 500 Mbps connection was super awesome back then).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yj97602uphpdar6sgc3.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yj97602uphpdar6sgc3.PNG" alt="Image description" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;
If you do the splitting up part over and over long enough, you will run out of space on your drawing board.



&lt;h2&gt;
  
  
  Going elastic
&lt;/h2&gt;

&lt;p&gt;Now I got a bunch of documents. It was an awesome and inspiring feeling! This was the point when I realized that the original archiving idea can be done on a grand scale. It was good to see a couple of hundred gigabytes of documents on my hard disk, but you know what would be better? Indexing them into a search engine, then having a way to search and view them.&lt;/p&gt;

&lt;p&gt;Initially I had little experience with indexing big datasets. I used &lt;a href="https://solr.apache.org/" rel="noopener noreferrer"&gt;Solr&lt;/a&gt; a while ago (like 7 years ago lol) so my initial idea came down to use that for the indexing. However, just by looking around for a bit longer before starting to work on the implementation I found &lt;a href="https://www.elastic.co/what-is/elasticsearch" rel="noopener noreferrer"&gt;Elasticsearch&lt;/a&gt;. It seemed to be superior over Solr in almost every way possible (except it was managed by a company but whatever). The major selling point was that it was easier to integrate with. As far as I know, both of them are just a wrapper around Lucene so the performance should be fairly similar. Maybe once it will be worthwhile to rewrite the application suite to use pure Lucene without actually doing &lt;a href="http://wiki.c2.com/?PrematureOptimization=" rel="noopener noreferrer"&gt;premature optimization&lt;/a&gt;. However, until then, Elasticsearch is the name of the game.&lt;/p&gt;

&lt;p&gt;After figuring out how indexing can be done, I immediately started to work. Extended the downloader application with code that indexed the downloaded and verified documents. Then I deleted the existing dataset to free up space, and started the whole downloading part yet again in the next night.&lt;/p&gt;

&lt;p&gt;The indexing worked remarkably well, so I started to work on a web frontend that could be used to search and view documents. This was (controversially) called as the &lt;em&gt;Backend Application&lt;/em&gt; in the beginning, then I quickly renamed it to the more meaningful name of &lt;em&gt;Web Application&lt;/em&gt;. I'll use that name in this document to minimize the complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going angular
&lt;/h2&gt;

&lt;p&gt;Initially, the frontend code was written in &lt;a href="https://github.com/angular/angular.js?" rel="noopener noreferrer"&gt;AngularJS&lt;/a&gt;. Why? Why have I choosen an obsolete technology to create the frontend of my next dream project? Because it was something I already understood quite well, was familiar with, and had a lot of experience in. At this stage, I just wanted to progress with my proof of concept. Optimizations and cleanups can be done later. Also, I'm a backend guy, so the frontend code should be minimal right? Right?&lt;/p&gt;

&lt;p&gt;It started out as minimal, that's for sure. Also, because it only used dependencies that can be served by &lt;a href="https://cdnjs.com/" rel="noopener noreferrer"&gt;cdnjs&lt;/a&gt;, it was easy to build and integrate into a Java application.&lt;/p&gt;

&lt;p&gt;Soon the frontend was finished and I had some time to actually search and read the documents I collected. I remember that I wanted to search something obscure. I was studying gardening back then, so my first search was for the lichen &lt;strong&gt;"Xanthoria parietina"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc11pdag1bys5ari66ue0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc11pdag1bys5ari66ue0.jpg" alt="Image description" width="300" height="211"&gt;&lt;/a&gt;&lt;/p&gt;
Xanthoria parietina&lt;br&gt;&lt;small&gt;&lt;a href="https://commons.wikimedia.org/wiki/User:Holleday" rel="noopener noreferrer"&gt;© Holger Krisp&lt;/a&gt; - &lt;a href="https://creativecommons.org/licenses/by/3.0" rel="noopener noreferrer"&gt;CC BY 3.0&lt;/a&gt; &lt;/small&gt;



&lt;p&gt;To my surprise, I got back around a hundred documents from a 2.3 million sample size. Honestly, I was surprised. Some of them were quite interesting. Like whom wouldn't want to read the &lt;em&gt;"Detection of polysaccharides and ultrastructural modification of the photobiont cell wall produced by two arginase isolectins from Xanthoria parietina"&lt;/em&gt;?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-3-1cih"&gt;Part three&lt;/a&gt; will describe how more challenges around the storage of documents were solved like deduplication and compression.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>learning</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>How I archived 100 million PDF documents... - Part 1: Reasons &amp; Beginning</title>
      <dc:creator>Gyula Lakatos</dc:creator>
      <pubDate>Wed, 11 Jan 2023 15:39:08 +0000</pubDate>
      <link>https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-1-1ha7</link>
      <guid>https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-1-1ha7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How I archived 100 million PDF documents..." is a series about my experiences with data collecting and archival while working on the &lt;a href="https://github.com/bottomless-archive-project/library-of-alexandria" rel="noopener noreferrer"&gt;Library of Alexandria&lt;/a&gt; project. My local instance just hit 100 million documents. It's a good time to pop a 🍾 and remember how I got here.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The beginning
&lt;/h2&gt;

&lt;p&gt;On a Friday night, after work, most people usually watch football, go to the gym or do something useful with their life. Not everyone though. I was an exception to this rule. As an introvert, I spent the last part of my day sitting in my room, reading an utterly boring-sounding book called &lt;a href="https://en.wikipedia.org/wiki/Epistulae_Morales_ad_Lucilium" rel="noopener noreferrer"&gt;"Moral letters to Lucilius"&lt;/a&gt;. It was written by some &lt;a href="https://en.wikipedia.org/wiki/Seneca_the_Younger" rel="noopener noreferrer"&gt;old dude&lt;/a&gt; thousands of years ago. Definitely not the most fun-sounding book for a Friday night. However, after reading it for about an hour, I realized that the title might be boring, but the contents are almost literally gold. Too bad that there were only a couple of these books that withstood the test of time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0w1u18isxnb8rfejov6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0w1u18isxnb8rfejov6.jpg" alt="Image description" width="300" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
Good ole' Seneca&lt;br&gt;&lt;small&gt;(image thanks to &lt;a href="https://www.flickr.com/photos/18637958@N08/3314905852/in/album-72157614433871807/" rel="noopener noreferrer"&gt;Matas Petrikas&lt;/a&gt;)&lt;/small&gt;



&lt;p&gt;After a quick Google search, I figured out that only &lt;a href="https://en.wikipedia.org/wiki/Lost_literary_work" rel="noopener noreferrer"&gt;less than 1% of ancient texts&lt;/a&gt; survived to the modern day. This unfortunate fact was my inspiration to start working on an ambitious web crawling and archival project, called the &lt;a href="https://github.com/bottomless-archive-project/library-of-alexandria" rel="noopener noreferrer"&gt;Library of Alexandria&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  But how?
&lt;/h2&gt;

&lt;p&gt;At this point, I had a couple (more like a dozen) failed projects under my belt, so I was not too fond to start working on a new one. I had to motivate myself. After I set the target of saving as many documents as possible, I wanted to have a more tangible but quite hard-to-achieve goal. I set 100 million documents as my initial goal and a billion documents as my ultimate target. Ohh how naive I was.&lt;/p&gt;

&lt;p&gt;The next day, after waking up, I immediately started typing on my old and trustworthy PC. Because I have a very T-shaped programming knowledge that is centered around &lt;strong&gt;Java&lt;/strong&gt;, the language of choice for this project was immediately determined. Also, because I like to create small prototypes to understand the problem I need to solve, I immediately started with one.&lt;/p&gt;

&lt;p&gt;The goal of the prototype was simple. I "just" wanted to download 10.000 documents to understand how hard it is to collect and kind of archive them. The immediate problem was that I didn't know where can I get links for this many files. &lt;a href="https://en.wikipedia.org/wiki/Site_map" rel="noopener noreferrer"&gt;Sitemaps&lt;/a&gt; can be useful in similar scenarios. However, there are a couple of reasons why in this case they are not really a viable solution. Most of the time it doesn't contain links to the documents, or at least not to all of them. Also, I would need to get a domain list to download the sitemaps for, etc. The immediate thing that came into my mind was that it is a lot of hassle and there must be an easier way. This is when the &lt;a href="https://commoncrawl.org/" rel="noopener noreferrer"&gt;Common Crawl&lt;/a&gt; project came into the view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common crawl
&lt;/h2&gt;

&lt;p&gt;Common Crawl is a project that contains hundreds of terabytes of HTML source code from websites that were &lt;a href="https://en.wikipedia.org/wiki/Web_crawler" rel="noopener noreferrer"&gt;crawled&lt;/a&gt; by the project. They publish a new set of crawl data at the beginning of each month.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The crawl archive for July/August 2021 is now available! The data was crawled July 23 – August 6 and contains &lt;strong&gt;3.15 billion web pages&lt;/strong&gt; or &lt;strong&gt;360 TiB of uncompressed content&lt;/strong&gt;. It includes page captures of 1 billion new URLs, not visited in any of our prior crawls.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36ldkuagubse1kpcw421.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36ldkuagubse1kpcw421.PNG" alt="Image description" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;
Tiny little datasets...



&lt;p&gt;It sounded exactly like the data that I needed. There was just one thing left to do. Grab the files and parse them with an HTML parser. This was the time when I realized that no matter what I do, it's not going to be an easy ride. When I downloaded the first entry provided by the Common Crawl project, I noticed that it was saved in a strange file format called &lt;a href="https://en.wikipedia.org/wiki/Web_ARChive" rel="noopener noreferrer"&gt;WARC&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I found one Java library on &lt;a href="https://github.com/Mixnode/mixnode-warcreader-java" rel="noopener noreferrer"&gt;Github&lt;/a&gt; (thanks Mixnode) that was able to read these files. Unfortunately, it was not maintained for the past couple of years. I picked it up and &lt;a href="https://github.com/laxika/java-warc" rel="noopener noreferrer"&gt;forked it&lt;/a&gt; to make it a little easier to use. (A couple of years later this repo was &lt;a href="https://github.com/bottomless-archive-project/java-warc" rel="noopener noreferrer"&gt;moved under&lt;/a&gt; the Bottomless Archive project as well.)&lt;/p&gt;

&lt;p&gt;Finally, at this point, I was able to go through a bunch of webpages (parsing them in the process with &lt;a href="https://jsoup.org/" rel="noopener noreferrer"&gt;JSoup&lt;/a&gt;), grab all the links that contained pdf files based on the file extension then download them. Unsurprisingly, most of the pages (~60-80%) ended up being unavailable (&lt;a href="https://en.wikipedia.org/wiki/HTTP_404#Soft_404" rel="noopener noreferrer"&gt;404 Not Found&lt;/a&gt; and friends). After a quick cup of coffee, I got the 10.000 documents on my hard drive. This is when I realized that I have one more problem to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unboxing &amp;amp; validation
&lt;/h2&gt;

&lt;p&gt;So, when I started to view the documents, a lot of them simply failed to open. I had to look around for a library that could verify PDF documents. I had some experience with &lt;a href="https://pdfbox.apache.org/" rel="noopener noreferrer"&gt;PDFBox&lt;/a&gt; in the past, so it seemed to be a good go-to solution. It had no way to verify documents by default, but it could open and parse them and that was enough to filter out the incorrect ones. It felt a little bit strange just to read the whole PDF into the memory to verify if it is correct or not, but hey I needed a simple fix for now and it worked really well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkpog69leh9t2kslgafj6.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkpog69leh9t2kslgafj6.PNG" alt="Image description" width="696" height="261"&gt;&lt;/a&gt;&lt;/p&gt;
Literally, half of the internet.



&lt;p&gt;After doing a re-run, I concluded that 10.000 perfectly valid documents can fit on around 1.5 GB of space. That's not too bad I thought. Let's crawl more because it sounds like a lot of fun. I left my PC there for about half an hour, just to test the app a bit more.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/laxika/how-i-archived-100-million-pdf-documents-part-2-ffg"&gt;Part two&lt;/a&gt; will describe how more challenges were solved like parallelizing the download requests, splitting up the application, making the documents searchable, and adding a web user interface.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>tailwindcss</category>
      <category>react</category>
      <category>nextjs</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
