<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Sortlist Engineering - Medium]]></title>
        <description><![CDATA[Sortlist Product, Engineering &amp; Data Science - Medium]]></description>
        <link>https://medium.com/sortlist-engineering?source=rss----926618b8ffdb---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Sortlist Engineering - Medium</title>
            <link>https://medium.com/sortlist-engineering?source=rss----926618b8ffdb---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 18:58:33 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/sortlist-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[The Tool That Turned Everyone Into a Contributor]]></title>
            <link>https://medium.com/sortlist-engineering/the-tool-that-turned-everyone-into-a-contributor-dbd9b075113b?source=rss----926618b8ffdb---4</link>
            <guid isPermaLink="false">https://medium.com/p/dbd9b075113b</guid>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[github]]></category>
            <category><![CDATA[slack]]></category>
            <dc:creator><![CDATA[Nicolaos Moscholios]]></dc:creator>
            <pubDate>Tue, 24 Mar 2026 09:52:29 GMT</pubDate>
            <atom:updated>2026-03-24T09:52:29.359Z</atom:updated>
            <content:encoded><![CDATA[<p>How at Sortlist we built a AI Slack bot that lets PMs, designers, and BI analysts ship code to production.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d7gtKrAJkemGt-_MJk4h8g.png" /></figure><p>One day our PM Laura noticed something strange. All over the app, “Sortlist” was being translated as a common noun “Sortierliste” in German, “liste de tri” in French. This was a translation issue spread across 4 languages and multiple strings in the code.</p><p>Normally the fix would have gone through the usual cycle: someone files a ticket, it gets prioritised, a developer blocks out time, goes through the translation files, opens a PR, gets it reviewed, merges it. But we all know how it goes, this sort of fix never gets prioritised. We know it exists in our backlog, but we are always busy on something else. This fix would have probably been addressed at the end of the week, if we’re being optimistic.</p><p>But here comes the catch: Laura typed one message in Slack and 10 minutes later there was a pull request with all the translations fixed. Combined with a strong CI setup, a single approval was enough to get this automatically merged and ship to production in the same day.</p><p>This was thanks to SARA, our Semi-Autonomous Remote Agent, and it’s the most impactful internal tool we’ve shipped at Sortlist yet.</p><h3>Why did we build this?</h3><p>We’re a small engineering team of around 10 supporting a complex platform: a Ruby backend and some microservices, 3 large Next.js frontends, and a handful of internal tools. The team is small but the codebase is not.</p><p>Sortlist also has a unique product. Being essentially the matchmaker between service providers and companies, we have to develop and maintain multiple applications that fill completely different business needs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DokZlMC2vcL8cu5U9rKRgg.png" /><figcaption>Highly simplified graph of our core services and how they interact</figcaption></figure><p>The problem isn’t that we are bad at shipping, but we just don’t have the capacity for many, small requests coming from different stakeholders. A large portion of the tasks that hit our backlog every week are small, well-defined, and could theoretically be handled by anyone with codebase access; think copy changes, translation fixes, minor UI tweaks, bug investigations triggered by Sentry alerts etc. But “codebase access” is the blocker. Firstly we can’t just give access to the code to <em>anyone</em>, plus non-developers would have no real clue how to get around the code.</p><p>We needed a way to collapse the gap between “I noticed something” and “it’s fixed.”</p><h3>What SARA Does</h3><p>SARA lives in Slack in a dedicated channel and people simply describe what they need. SARA reads the conversation and figures out what’s being asked then routes the request to the right workflow. It’s also plugged in Github to all our pull requests, meaning developers can take advantage of this tool as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*3YjUT1u_5FY4G6wfH_ec3Q.gif" /><figcaption>Fixing an open Notion ticket, SARA creates related pull requests on backend and frontend</figcaption></figure><p>Here are six things it can do:</p><ul><li><strong>Investigate a bug</strong>: Given a Sentry error, a Notion ticket, or just a description of unexpected behaviour, it digs through the codebase, checks logs via Grafana, and produces a root cause analysis with a Notion bug ticket attached</li><li><strong>Fix a bug</strong>: Once a Notion ticket exists (which SARA may have just created), you can ask it to fix the issue. It reads the ticket, understands the codebase, writes the fix with tests, and opens a PR. A developer just needs to review and approve. Automerge is enabled and if all CI checks are green, it’s merged.</li><li><strong>Review a PR</strong>: As mentioned before, SARA is also able to react to actions within Github. For example, a developer may request a review before asking colleagues to get a first pass on the submitted changes. What’s nice about SARA’s reviews compared to existing tools like CodeRabbit, is that it can actually approve or request explicit changes.</li><li><strong>Fix a PR</strong>: If CI is red or a reviewer has left comments, you can hand the PR back to SARA. It analyses the failure logs, applies the requested changes, pushes new commits, and responds in the thread when done. Again, this can be done within Github simply by tagging the bot e.g. “@sara[bot] fix the CI failures” or if someone requests changes “@sara[bot] apply the change requests from the review”. This makes it much faster for developers to keep working on their main task while maintaining velocity on their other tasks.</li><li><strong>Answer questions about the codebase:</strong> For questions like “how does the matchmaking algorithm work?” or “how are opportunity prices computed?”. In this case SARA starts an interactive expert session, a persistent container that stays alive while you ask follow-up questions, then closes itself after 30 minutes of inactivity.</li><li><strong>Handle general tasks:</strong> Anything that doesn’t fit the above: writing documentation, small feature changes, updating copy, refactoring a module. If it’s something a developer could do with codebase access, SARA can probably do it too, although with some limitations.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*xkIJV76XQMtyFgQ6QcaIjA.gif" /><figcaption>Reviewing a PR — SARA can outright request changes or approve</figcaption></figure><h3>How it works</h3><p>The architecture is very simple. There are two moving parts: a coordinator and a worker</p><p>The <strong>coordinator</strong> is a Node.js process running on a single EC2 instance. It connects to Slack via Socket Mode, and listens to Github for events. When someone mentions SARA, the coordinator reads the full thread history, extracts any URLs (GitHub PRs, Notion pages, Sentry errors, links to other Slack threads), and sends everything to Claude Sonnet. Claude reads the context and returns a decision including which task type to run and what command to execute.</p><p>The <strong>worker</strong> is an ephemeral Docker container. All of the code for this custom agent lives within our main repository, meaning it has access to not only the source code which is included when the Docker image is built, but also all our AGENTS.md, custom rules, skills and commands we setup to use with Claude locally. It also has access to all of the MCPs we set up such as GitHub, Notion, Sentry, Grafana, Figma etc… and because the agent uses Claude CLI under the hood, it can harness all of these configurations too, just like our developers would. When the coordinator decides to run a task, it spins up a fresh container, passes in the command, and waits. The worker runs Claude Opus and limited to 200 tool calls, to avoid infinite execution. When it finishes it posts the result back to the coordinator, which posts it back to Slack as a message if necessary.</p><p>The worker is destroyed after every task, but for interactive Q&amp;A sessions, the worker stays alive on a fixed port and receives follow-up messages directly, without spawning a new container each time. This is just so the UX in the chat feels more snappy, and to keep context alive between every back and forth. After some amount of inactivity, the worker is destroyed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*QLobHLBTr0GvtJCxWXAnrQ.gif" /><figcaption>Here I asked SARA to explain how it works — being embedded in the codebase gives the bot access to all the source code, meaning it can technically improve itself</figcaption></figure><h3><strong>What surprised us</strong></h3><p>We knew SARA would be useful to developers through its Github integration, and that it would be used by non-developers, but we had not realised the impact it would have on our output.</p><p>Over the first three weeks in production, 9 people sent requests to the bot. The top requesters were PMs, Senior Designers and BI analysts, as well as our Head of Engineering and Head of Product. Of the total requests we tracked (around 50), 65% came from non-developers.</p><p>Laura asked it to fix translations, update copy on a feature page, investigate a billing bug, explain how opportunity prices are computed for a customer conversation, and implement a new CTA from a Notion spec. Not once did she have to open a terminal.</p><p>One of our designers described a UI change he wanted and provided the spec in plain Slack messages. SARA implemented it, he tested on sandbox, gave feedback, SARA iterated. It took a few rounds and cost $6.76 in token usage, but still got shipped.</p><p>Our Data Specialist needed technical documentation for 8 database tables. He uploaded a diagram, pointed SARA at the relevant Notion page, and 9 minutes later had a full schema document pushed directly to Notion. That task would never have been prioritised before SARA existed.</p><p>The common thread across all of these examples is that no one had a local dev setup. No git, no IDE, no understanding of the repo structure. They just described what they wanted, in the same way they’d describe it to a colleague.</p><h3>Some interesting numbers</h3><p>In 18 days of production use, SARA opened 48 pull requests across three repositories. 31 were merged (65% merge rate), meaning most of what it produces is production-ready with only a light human review. It also submitted 55 reviews on PRs written by developers, and created 2 Notion bug tickets autonomously from investigations.</p><p>The merge rate is the number we watch most closely. 65% means the bot is genuinely useful. The 35% that doesn’t get merged either required a human intervention and what was produced was used as a base, or simply got scrapped. A lot of the early attempts were tests, so these numbers will hopefully improve as time goes by.</p><h3>What we learned</h3><p>The Slack interface was the 10x decision. We could have built a CLI, a web UI, or a GitHub bot that responds to issue labels; and in fact we started there at first. That would have kept the tool between developers only, but we’re already so busy with other tasks, and we can only parallelise so much with our own local Claude running in our terminal.</p><p>Also, specialised workflows matter. SARA has distinct workflows for bug investigation, bug fixing, PR review, and PR fixing. Each has its own system prompt, tool access, and output format. A generic “do anything” agent produces inconsistent results. The specialised workflows are what make the output reliable enough to trust in a PR. It’s good to note that these workflows are simply custom Claude “commands” we can invoke locally as well.</p><p>Iteration is normal and that’s fine. Sometimes SARA takes three rounds to get the CI green. Sometimes it ships something that needs a follow-up fix. And at times where a lot of UI is involved it’s still not perfect (browser inspection is something we have on the horizon). But even three bot iterations is usually faster than one developer context-switch.</p><p>While the bot can outright approve PRs, we still try to have a human review the changes first. That said, we’re noticing more and more that the bot approval + nitpick comments are usually good enough to ship.</p><p>This whole project is a result of the team investing time and energy into scaling our AI-first processes. Our first iteration was making sure local tools like Claude were able to generate code and fix issues following our conventions. We implemented solid AGENTS.md, rules and custom skills to make us more efficient.</p><p>Then, we thought, if almost all of what we’re doing now is prompting our local Claude to generate code for us, with a little extra help, can’t anyone do the same? As we set out to make our developers faster, we accidentally made everyone a contributor.</p><h3>What’s next</h3><p>As you might have noticed, SARA is indeed still “semi-autonomous”. We kept the triggers purposefully manual for now. Why?</p><ul><li><strong>Trust</strong> <strong>earned</strong> <strong>incrementally</strong>: we want to make sure SARA’s output can be trusted and is of good quality before letting it loose</li><li><strong>Human</strong> <strong>intent</strong> <strong>as</strong> <strong>a</strong> <strong>quality</strong> <strong>signal</strong>: humans still decide when something is worth pursuing, decreasing the risk for unnecessary changes</li><li><strong>Controllable</strong> <strong>pace</strong> <strong>of</strong> <strong>change: </strong>our CI is solid, but not yet fully tailored for AI-driven changes, we still need humans to review SARA’s work and cannot risk becoming the bottleneck</li></ul><p>For SARA to become fully autonomous, we need to make sure it can go from request to release without <em>(nearly any)</em> human intervention. How do we do that? The results it spits out need to be very close to our expectations, and our automated review has to be extremely good at spotting issues. SARA would also need a way to test full user flows before going to production, something only our devs can do right now.</p><p>Eventually, the bot can even learn from its own mistakes, improving the internal system prompts by looking at its performance. For example, it would be able to analyse how many code changes got rejected, how many conversations ended because it investigated the wrong thing, and change its own prompts to do better next time. Right now someone has to say “hey SARA, fix this”. Soon, SARA will be the one saying “hey, I noticed this, want me to fix it?”</p><p>We could also plug it directly to our Notion ticketing system so that it can pick up work without us needing to mention it. We’re not far from that, but we want the 65% merge rate closer to 85% before we let it.</p><h3>Some closing thoughts</h3><p>It’s tempting to look at SARA and extrapolate: if a bot can ship 31 PRs in 18 days, why not let a fleet of agents run the entire backlog? The honest answer is that we’re nowhere near that, at least not at Sortlist.</p><p>SARA is great at well-scoped, well-defined work. But the hardest parts of software engineering were never about writing the code. They’re about understanding why a system was built a certain way, knowing which corners you absolutely cannot cut, and making trade-offs that account for things like infrastructure cost, data privacy, and historical decisions. That context lives in people’s heads, not in the codebase.</p><p>We’ve seen enough of the hype cycle to know the difference between a useful tool and a fantasy. A team of unsupervised agents shipping code 24/7 sounds impressive until you realise no one is asking whether the code <em>should</em> be shipped. The value of SARA isn’t that it removes developers from the equation, it removes developers from the work that was never a good use of their time in the first place!</p><p>The developers on our team are still the ones designing systems, making architectural decisions, debugging the gnarly production issues, and reviewing what SARA produces. We didn’t build SARA to replace that work, we built it to protect the time we need to spend on it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dbd9b075113b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/sortlist-engineering/the-tool-that-turned-everyone-into-a-contributor-dbd9b075113b">The Tool That Turned Everyone Into a Contributor</a> was originally published in <a href="https://medium.com/sortlist-engineering">Sortlist Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What product teams can learn from getting to the top 4% of the video game League of Legends]]></title>
            <link>https://medium.com/sortlist-engineering/what-product-teams-can-learn-from-getting-to-the-top-4-of-the-video-game-league-of-legends-550f5a338e06?source=rss----926618b8ffdb---4</link>
            <guid isPermaLink="false">https://medium.com/p/550f5a338e06</guid>
            <category><![CDATA[teamwork]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[decision-making]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[videogames]]></category>
            <dc:creator><![CDATA[Robin Rampaer]]></dc:creator>
            <pubDate>Tue, 24 Aug 2021 07:53:14 GMT</pubDate>
            <atom:updated>2021-08-24T11:44:56.827Z</atom:updated>
            <content:encoded><![CDATA[<h3>The one thing product teams can learn from getting to the top 4% of the video game League of Legends</h3><p><strong>TLDR:</strong> <em>It’s better to have the whole team aligned on a 80% optimal approach than unaligned on the perfect one.</em></p><h3>I learned one thing from playing too much video game and ending up in the top 4%</h3><p>During my time at university I played a ton of League of Legends - a competitive game where you’re matched with 4 strangers with the goal of beating 5 other players of similar strength.</p><p>I was never meant to be any kind of good: I have sloppy reflexes and poor hand precision.</p><p>However, I quickly realized that I could tip the scale by pushing my team to <strong><em>commit in unison to the same decision.</em></strong></p><p>Throughout a game there are tons of micro-decisions: do you stay and fight, do you attack a tower or go for an objective in “the jungle”, etc,.</p><p>I would try and make sure the team acted as an unit: take the lead when nobody called the shots, or follow when someone was taking a chance somewhere. This, <strong>even when I didn’t agree with that very decision</strong>. More often than not, <strong>our cohesive unit prevailed their scattered reactions.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PNjFrRrs0owRnZ_EMlhjKQ.png" /><figcaption>x9 Teemo pls</figcaption></figure><h3>In product team there are always options and trade-offs</h3><p>Similar scenarios tend to happen within product teams: engineers having to decide on a technical stack, designers having to set brand guidelines or product managers needing to formalize tracking conventions.</p><p>All of these decisions will have several reasonable options, each with their own tradeoffs.</p><p>A recent challenge we’re facing is how we’re going to style our Next.js frontend applications:</p><ul><li><strong>Extend Material-UI</strong>, but having to wait for the v5 version.</li><li><strong>Use Tailwind</strong>, to benefit from its purge.</li><li><strong>Use <em>styled-components</em></strong></li><li><strong>Use <em>emotion</em></strong></li><li>…</li></ul><p>We’ve been arguing then agreeing, then arguing again, going back to what we had said, challenging what has been done, etc,.</p><p>This is a case where we’re now striving for alignment rather than for perfection.</p><h3>Success depends more on the team’s alignment than on the solution’s perfection</h3><blockquote>“Agree and commit, disagree and commit, or get out of the way”<br>- <a href="https://en.wikipedia.org/wiki/Scott_McNealy">Scott McNealy</a> circa 1983</blockquote><p>This is also one of <a href="https://www.amazon.jobs/en/principles">Amazon’s principles</a>:</p><blockquote><strong><em>Have Backbone; Disagree and Commit </em></strong>— Leaders are obligated to respectfully challenge decisions when they disagree, even when doing so is uncomfortable or exhausting. Leaders have conviction and are tenacious. They do not compromise for the sake of social cohesion. Once a decision is determined, they commit wholly.</blockquote><h4>We are hiring</h4><p>If you’re ready to commit, whether or not you agree on the choice to be made, have a look at the opportunities we have within our product and engineering team:</p><ul><li><a href="https://sortlist.breezy.hr/p/7e290077c24101-devops-infrastructure-engineer">DevOps / Infrastructure Engineer</a></li><li><a href="https://sortlist.breezy.hr/p/248fa323bfa201-mid-senior-frontend-developer">Frontend Developer</a></li><li><a href="https://sortlist.breezy.hr/p/81bbe83616b301-ruby-backend-web-engineer">Ruby Backend Developer</a></li><li><a href="https://sortlist.breezy.hr/p/6155e502f63501-senior-product-designer">Senior Product Designer</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=550f5a338e06" width="1" height="1" alt=""><hr><p><a href="https://medium.com/sortlist-engineering/what-product-teams-can-learn-from-getting-to-the-top-4-of-the-video-game-league-of-legends-550f5a338e06">What product teams can learn from getting to the top 4% of the video game League of Legends</a> was originally published in <a href="https://medium.com/sortlist-engineering">Sortlist Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to do usability testing to improve your user experience]]></title>
            <link>https://medium.com/sortlist-engineering/how-to-do-usability-testing-to-improve-your-user-experience-18682b74801e?source=rss----926618b8ffdb---4</link>
            <guid isPermaLink="false">https://medium.com/p/18682b74801e</guid>
            <category><![CDATA[ux]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[product-design]]></category>
            <category><![CDATA[ux-design]]></category>
            <category><![CDATA[usability-testing]]></category>
            <dc:creator><![CDATA[Robin Rampaer]]></dc:creator>
            <pubDate>Wed, 18 Aug 2021 15:26:40 GMT</pubDate>
            <atom:updated>2021-08-19T15:38:45.605Z</atom:updated>
            <content:encoded><![CDATA[<h3>Your users are getting lost and confused</h3><p>Your app or website might be super valuable to your users, but if they can’t quickly figure out how to use it, many will give up and leave.</p><p>This is what happens on most government websites, you can’t really escape filling in your tax report, but it feels as if they had tried to make it as hard as possible for you to achieve what you thought would be a 2-minute ordeal. Unfortunately for you, using your app is probably not as important to your users as not getting a massive fine.</p><figure><img alt="user-testing-iphone" src="https://cdn-images-1.medium.com/max/1024/1*x4Nm4ccHh0Zns0aBlcX_WA.jpeg" /></figure><h3>Usability tests are an easy way to reduce the risk of losing users</h3><blockquote><em>Usability testing is a qualitative way of figuring out if your target customers can use the solution you have in mind.</em></blockquote><p>The usability risk is high in most client-facing UI and can and should be addressed as early as possible.</p><h3>Test before writing a single line of code</h3><p>Testing on a prototype can help you save 100’s of hours of coding / changing and deleting features later on.</p><p>The earlier you learn, the cheaper the changes are.</p><p>Because it’s so important, a lot of best practices have emerged, making this the easiest type of customer interview.</p><p>So go outside or send a few emails, find some users ready to take 15 minutes with you, and get testing.</p><h3>How to run a test</h3><h4>Set proper context:</h4><ol><li>Thank them for taking parts</li><li>Ask if it’s still ok to take XX mins and if ok for you to record it (if applicable)</li><li>Mention that the goal is to perform a set of tasks, not to focus on design details</li><li>Reassure them by saying that you’re testing the app not them</li><li>Inform them that this is a test environment (or mockup) so can’t break anything and it’s normal if some things are not working</li><li>Ask if they have any question, if not, let’s get started</li></ol><h4>Perform and assess the tasks:</h4><ol><li>Those tasks should be based on a set of pre-defined assumptions. You want to test for the most crucial / risky flows.<br>Ex: <em>“We think it’ll be obvious to the user where to find their message once sent”</em></li><li>Instruct the person to perform the tasks</li></ol><p><strong>Don’t:</strong> Tell them where to click, what to look for</p><blockquote><em>“</em><strong><em>Click</em></strong><em> the message button” ❌</em></blockquote><p><strong>Don’t:</strong> Use the exact copy of the UI</p><blockquote><em>“Please </em><strong><em>send a message</em></strong><em> to Capital Panache” ❌</em></blockquote><p><strong>Do:</strong> Tell them what to achive in everyday language</p><blockquote><em>“Please </em><strong><em>get in touch with</em></strong><em> Capital Panache” ✔️</em></blockquote><p><strong>Don’t:</strong> Answer their question</p><blockquote><strong><em>User:</em></strong><em> “What happens if I click the button?”<br></em><strong><em>You:</em></strong><em> “Oh, that sends a message to the agency” ❌</em></blockquote><p><strong>Do:</strong> Ask what they think in order to understand their assumptions</p><blockquote><strong><em>User:</em></strong><em> “What happens if I click the button?”<br></em><strong><em>You:</em></strong><em> “Mmmh, what do you think?” ✔️<br></em><strong><em>User:</em></strong><em> “I guess it’ll open some kind of inbox”<br></em><strong><em>User:</em></strong><em> “Like in a popup kinda like messenger on Facebook”<br></em><strong><em>You:</em></strong><em> “Oh ok, well, can you try?” ✔️</em></blockquote><p><strong>Do:</strong> Ask what were their thoughts when you read interesting body language</p><blockquote><strong><em>User:</em></strong><em> *Frown* then *looks like EUREKA!*<br></em><strong><em>You:</em></strong><em> “Ah! Have you discovered something?” ✔️<br></em><strong><em>User:</em></strong><em> “YES! I thought it was a cancel but then I saw the red label so I understood it was more of a delete?”</em></blockquote><p><strong>Note whether each task was:</strong></p><ul><li><em>Succesful</em> → Found and perfomed the target action without problem</li><li><em>Adding Friction</em> → A bit hard but could manage</li><li><em>Blocking</em> → Could not perform it without external help</li></ul><h3>Wrap up</h3><ol><li>Thank them again for taking part</li><li>Inform them what are your next step, if you plan on communicating back to them, mention it and ask what channel would be suit them</li><li>Ask if they have any extra feedback / question / ideas</li></ol><h3>Capture and act upon the learnings</h3><p>Don’t rush! It usually takes 5–6 person to see trends appear. Don’t fix everything you see before either being convinced yourself or seeing it happen multiple times.</p><p><strong>Summarize the learnings</strong><br>A great framework is to have the following assessment for each key action:<br><strong><em>We thought that</em></strong> … <strong><em>We saw that</em></strong> … <strong><em>Therefore we will</em></strong></p><p>For example:</p><ul><li><strong><em>We thought that</em></strong> <em>the user would understand that search was performed on each action</em></li><li><strong><em>We saw that</em></strong> <em>most users don’t understand the search has been performed and look for a search CTA</em></li><li><strong><em>Therefore we will</em></strong> <em>add the CTA while keeping the hot refresh</em></li></ul><p><strong>Make it painful:</strong> People don’t have your context and will likely doubt your findings, therefore it’s great to have videos or data to highlight the main problems encountered and make everyone feel the pain of the users.</p><h3>Want to get started or go further?</h3><h4>Join our team</h4><ul><li>🆕 We’re looking for a <a href="https://sortlist.breezy.hr/p/6155e502f63501-product-designer">Senior Product Designer</a></li></ul><h4>Related content</h4><ul><li>📺 <a href="https://youtu.be/Qq3OiHQ-HCU">How to run a usability test (5 min)</a></li><li>📺 <a href="https://www.youtube.com/watch?v=qAws7eXItMk">How to Run a User Interview (46 min)</a></li><li>📄 <a href="https://www.usability.gov/how-to-and-tools/methods/reporting-usability-test-results.html">Reporting Usability Test Results</a></li></ul><h4>Interesting Tools</h4><ul><li><a href="https://maze.design/">Maze.design</a> is a slick tool allowing you to do remote asynchronous usability testing. We’ve done this with our Invision or Figma prototypes.</li><li><a href="https://www.markup.io/">Markup.io</a> is an easy way to get feedback on a live website.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=18682b74801e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/sortlist-engineering/how-to-do-usability-testing-to-improve-your-user-experience-18682b74801e">How to do usability testing to improve your user experience</a> was originally published in <a href="https://medium.com/sortlist-engineering">Sortlist Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How a text recommendation system can be the functional core of a product]]></title>
            <link>https://medium.com/sortlist-engineering/how-a-text-recommendation-system-can-be-the-functional-core-of-a-product-84ca94c21875?source=rss----926618b8ffdb---4</link>
            <guid isPermaLink="false">https://medium.com/p/84ca94c21875</guid>
            <category><![CDATA[natural-language-process]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[complex-networks]]></category>
            <category><![CDATA[marketing]]></category>
            <dc:creator><![CDATA[Bogdan Mursa]]></dc:creator>
            <pubDate>Wed, 11 Dec 2019 15:20:45 GMT</pubDate>
            <atom:updated>2019-12-12T10:28:11.316Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RLGWFtPp5eEJKWdM1evrZw.jpeg" /><figcaption><a href="http://astropixels.com/milkyway/starscapes/MilkyWayPan02.html"><strong>“We all travel the Milky Way together, trees and men.</strong></a><strong>” — John Muir</strong></figcaption></figure><p>This article is willing to educate the readers about how a text recommendation system can be the central pillar of the matching processes in a product.</p><p>The discussion will have as the main <em>actor</em> a recommendation system built using the Word2Vec framework and reinforced with concepts from Complex Network Theory which aims to provide accurate matches between marketing projects and marketing agencies.</p><p>The volume of information flowing virtually through the WWW network increases proportionally with the number of websites that appear in any given moment. It would be cliché to say that companies on any scale do not understand the importance of the information that their products can capture, as most of them are following a data-driven approach while taking business decisions.</p><blockquote>“Data is the new oil” — the mantra of the current technological era.</blockquote><p>There is an immeasurable number of applications that can use data to model the desired feature — computer vision, deep learning or natural language processing are just a few examples of domains where data plays a significant role. These applications are usually used to automate business processes or facilitate the user experience of products features.</p><p>This blog post is willing to represent an overview of how designing such an application facilitates the user experience of products that have text data as a primary source of information. Either for matching, information retrieval or classification purposes, a properly build text recommender system can be a powerful toolset with solutions for all these challenging tasks. What I aim is to educate the audience about how it is possible to build and use such a solution, but do not expect a step-by-step tutorial. I found it more valuable to approach the subject from an architectural perspective while summarising or just reminding low-level technique aspects and focus on the text recommendation system as a tool that can be integrated into multiple areas of a product.</p><p>What you will read in the upcoming minutes, represents my personal experience in designing such a text recommendation system for Sortlist, a company with a product that is aiming to help businesses finding the best marketing agencies in the world for their marketing projects. When we think about a marketing project, we need to imagine a briefing that contains information such as budget, location, desired skills, desired interests, and many others. Most of this information can be used in simple filtering operations, but unfortunately, only one type of requirements fall into the chaotic world of fuzziness: skills.</p><p>Most of the times companies do not know exactly what they are looking for from a technical perspective — because a briefing can be written either by a small shop owner willing to create a website to promote its products or by an SEO expert which is looking for a team of developers to refactor a website to be aligned with the newest SEO trends.</p><p>Considering this impediment in customer behavior, the goal was to design a technical proposal that could be a general solution for multiple use cases on the platform, either on matching or recommendation features.</p><p>Generally, any kind of text information is interpretable only by a human being as it has features strongly tied to linguistics aspects. Hence we need to find a way to transpose a text into a structure understandable by the computers as well. A solution to this particular problem is offered by natural language processing through <a href="https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf">word embedding</a> — a technique that helps to represent any given word into an N-dimensional vector of features. An N-dimensional vector represents the position of a word in a space with the same numbers of dimensions.</p><figure><img alt="Example of a 2-dimensional projection of a space trained using texts about world countries and their capitals." src="https://cdn-images-1.medium.com/max/1024/1*sDDpUpa_WP5E8qNGvUFzOg.png" /></figure><p>Figure: <a href="https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf"><em>Two-dimensional projection of a vector space trained using texts about world countries and their capitals</em></a></p><p>Simplifying the concept we can make an analogy with the geographic coordinate system which enables which enables to pinpoint any location on Earth with 2-dimensional vectors named coordinates (latitude and longitude). Moreover using this representation in space it is possible to compute the distances between any two coordinates.</p><p>Keeping in mind this analogy for a simpler 2-dimensional space, it is possible to compute distances between any two points in spaces with higher dimensions as well, namely N-dimensional spaces. Hence, putting our imagination at work we can visualize how a bunch of words into an N-dimensional space are closer or not to each other depending on their linguistic similarity. Ok, pause — It is only me or it was impossible for my brain to visualize the N-dimensional space? Right, the human brain is able to comprehend only 3 dimensions, so we will have to deal with the N-dimensions as an abstract mathematical space.</p><p>To create the N-dimensional space, namely the vector space, we need a training set and a framework that is able to embed the words from the training set into the vector space. A crucial subject that I feel is superficially discussed in most of the text recommendation tutorials is preparation of the training set — we mentioned how each word is transposed into a vector of features which can be seen as an N-dimensional fingerprint of that, but we never mentioned how the list of features is computed. There is no simple way to explain it but saying that the features represent the context of a word in the training set. Hence the same word can have different “neighbors” in a vector space modeled with marketing texts than in a vector space created with literature texts for example. This behavior will be even more prominent for words that are specific to a given domain. Long story short, it is strongly recommended to have a training set with content specific to the field where the model will be used.</p><p>Having a proper training set, it is recommended to follow a sequence of preprocessing steps from which I can remind: word normalization (<a href="https://www.datacamp.com/community/tutorials/stemming-lemmatization-python">lemmatisation or stemming</a>), <a href="https://programminghistorian.org/en/lessons/keywords-in-context-using-n-grams">n-gram detection</a> (important for entities with multiple words), non-alphanumeric characters removal and many others. The processed text can be fed into a word embedding tool that can train the vector space — the state-of-the-art being Word2Vec, a two-layered shallow network proposed by <a href="https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf">Tomas Mikolov</a>. There are various implementations of it, personally, I used <em>Python’s </em><a href="https://pypi.org/project/gensim/"><em>gensim</em></a><em> </em>library which has an friendly interface (compute the similarity between 2 words, compute top N similar words for a given word, arithmetic operations between word vectors, etc.).</p><p>Going back to my case, although I managed to obtain a satisfying vector space from the point of view of marketing terminology accuracy, I was still not happy with the results. I wanted to visualize my creation. Because we are facing N-dimensions there is no easy way to create a plot or something else to facilitate the visual projection of the space. A state-of-the-art solution would be principal component analysis (PCA) — a dimensionality-reduction technique, but I wanted something else, something more visually expressive and one that could facilitate the navigation in the space.</p><p>After some time spent for looking a candidate, I realized that I already had a solution the whole time: the area that I intensively study for my Ph.D., Complex Network Theory — a field that proposes the modeling of real-world systems as topological spaces similar to mathematical graphs. The beauty of these structures is provided by the infinite degree of analysis one is allowed to do using them using the framework proposed by this field. So without thinking twice, I modeled the vector space as a complex network and using an <a href="https://github.com/anvaka/word2vec-graph">amazing tool</a>, obtaining <strong>Milkyway</strong> — a galaxy where each planet represents a skill and an edge between two planets is given by the distance of the corresponding skills the N-dimensional vector space:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*5fnZo-PyUN_YReNH9nRaIg.gif" /></figure><p>Using both Word2Vec trained space and Milkyway (the complex network) we were able to create a remarkable tool with a perspective of appliance in most of the areas of the core business product. Using Word2Vec interface to compute similarities and a bunch of properties proposed by the Complex Network Theory such as communities, shortest paths, motifs or open triangles (all widely studied in Complex Network Theory) it was possible to immediately develop features in the product that had a significant impact in the user experience or preciseness of the obtained results in different processes.</p><p>Having Milkyway and its visual representation as a complex network it was easier to facilitate close cooperation with the business people and identify together useful applications of the model. The following paragraphs represent a discussion over a few areas where the model was used:</p><p><strong>Suggestions — </strong>One of the key processes of the platform is when users select a list of skills either as a company that proceeds with a project briefing or as an agency that creates a profile accordingly to their expertise. Because the users can have a different level of knowledge in the field, sometimes it is hard for them to choose the right skills. With the proposed tool it was possible to develop a suggestion system that receives as input a list of skills (chosen by the user at any given moment) and returns as output a list of suggested skills — knowing that similar skills will be grouped in the so-called communities, it was possible to return a precise answer by combining results of the vector space distances with the shortest paths and open triangles of the input skills in the modeled complex network.</p><p><strong>Matching</strong>: Using the same principles as for <strong>suggestions</strong>, the tool is able to match project briefings skills (A) with agencies’ skills (B) while applying a procedure of intersection between space A and space B. In this way, the matching algorithm becomes smart enough to match complicated combinations of skills. As an example, if someone that wrote a project briefing needs an agency with experience in <em>Google Ads, </em>besides a straightforward match with the agencies that have <em>Google Ads </em>in their profile, the model will also match any agency that have <em>SEA</em> in their profile skills, <em>SEA </em>being a marketing skill related to <em>Google Ads</em> — the same principle applies for any other skill found in the proximity area of <em>Google Ads.</em><strong><em> </em></strong>Having more<strong><em> </em></strong>skills to match means that the results will be more precise as the overlapping of the intersections will be smaller.</p><p><strong>Marketing — </strong>Combining information retrieved from Milkyway and Google search volumes, Sortlist is able to automatically generate landing pages for the most relevant and in trend skills at any given moment. This has a significant impact on the traffic of the platform leading to an increase in demand and supply, hence making happy every type of user of/on the platform.</p><p>These were just a few examples of applications of the tool we call <strong>Milkyway </strong>which already stands as an important asset for Sortlist project. I hope this article gave you a better understanding of how such a system can be used in real products and the benefits they bring in processes that they are integrated in.</p><p><em>TL;DR 😇<br>Interested in joining the product &amp; engineering team of a fast-growing tech start-up and you want to use top-notch technology? Sortlist is always looking for talented people to join us in Belgium or Romania.</em></p><p><em>Check out our positions here:</em></p><p><a href="https://sortlist.breezy.hr/">Become our new colleague!</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=84ca94c21875" width="1" height="1" alt=""><hr><p><a href="https://medium.com/sortlist-engineering/how-a-text-recommendation-system-can-be-the-functional-core-of-a-product-84ca94c21875">How a text recommendation system can be the functional core of a product</a> was originally published in <a href="https://medium.com/sortlist-engineering">Sortlist Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating from Paperclip to ActiveStorage: a different approach]]></title>
            <link>https://medium.com/sortlist-engineering/migrating-from-paperclip-to-activestorage-a-different-approach-4bffa2260e17?source=rss----926618b8ffdb---4</link>
            <guid isPermaLink="false">https://medium.com/p/4bffa2260e17</guid>
            <category><![CDATA[paperclip]]></category>
            <category><![CDATA[active-storage]]></category>
            <category><![CDATA[ruby-on-rails]]></category>
            <category><![CDATA[amazon-s3]]></category>
            <dc:creator><![CDATA[Mihai C]]></dc:creator>
            <pubDate>Tue, 10 Sep 2019 09:34:54 GMT</pubDate>
            <atom:updated>2020-02-20T08:17:06.581Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/837/1*9aJNF5G6bXbJ8VtCvSx79Q.png" /></figure><p>In this article we will discuss how to migrate hundreds of thousands of attachments from Paperclip to ActiveStorage without downtime.</p><p>At <a href="https://www.sortlist.com/">Sortlist</a>, one of my first tasks after joining the team, was to migrate from <a href="https://github.com/thoughtbot/paperclip">Paperclip</a> to <a href="https://rubyonrails.org/">Rails</a> built-in <a href="https://edgeguides.rubyonrails.org/active_storage_overview.html">ActiveStorage</a> or to <a href="https://github.com/shrinerb/shrine">Shrine</a> (another candidate can be <a href="https://github.com/carrierwaveuploader/carrierwave">CarrierWave</a>). The reason for this is because we are adepts of keeping things up to date and Rails 5.2 came with ActiveStorage. Paperclip was already deprecated for some time and we wanted to move on with our lives ✈️.</p><p>We came to the conclusion that we would migrate to ActiveStorage because we don’t really need the full processing power and configuration options of Shrine. Even though (at the time of writing this article) ActiveStorage is not as mature as Shrine or CarrierWave, it has the Rails community behind it, so we were happy with that.</p><p>After reading a few articles about this on the web, the process seemed pretty straightforward, but the problem was that all the tutorials that we have found were dedicated to small Ruby on Rails applications with a limited number of attachments. In these cases, the migration is very fast with no downtime whatsoever. Some examples: <a href="https://gorails.com/episodes/migrate-from-paperclip-to-rails-active-storage">GoRails</a> and the <strong>RailsConf 2019</strong> video below.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FtZ_WNUytO9o%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DtZ_WNUytO9o&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FtZ_WNUytO9o%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/ed13a1571799c02509d2bfca44f71bb6/href">https://medium.com/media/ed13a1571799c02509d2bfca44f71bb6/href</a></iframe><p>Since at <a href="https://www.sortlist.com/">Sortlist</a> we have hundreds of thousands of attachments, we can’t afford to wait hours or possibly even days to migrate all of our attachments and during this time to have all attachment actions unavailable 😱, so we had to come up with a different approach and this is where things become interesting. I’ll start explaining below, but first let’s refresh our knowledge on how Paperclip and ActiveStorage work.</p><p>As you may know, Paperclip works by attaching file data to the model by changing the schema of the model. In addition to that, you can also write normal Rails validations for attachments. For example (or some other variations):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/81bc930dbf39a5ba73d4e48732947d88/href">https://medium.com/media/81bc930dbf39a5ba73d4e48732947d88/href</a></iframe><p>If you need more attachment types on a specific model, than your model schema can become out of hand. 🙊</p><p>On the other hand, ActiveStorage creates 2 new database tables ActiveStorageBlobs which is the table handling the attachment data and ActiveStorageAttachments<em> </em>table<em>,</em> which is a polymorphic table between the blobs table and your Rails models. This means you can also have as many attachment types as you want on your model, without every changing the schema 🔝.</p><p>So now that we know how they work and because we didn’t want to have any unavailable attachments during the whole migration process, we decided to split the migration into 2 big steps or Pull Requests: <strong>Hidden ActiveStorage </strong>and <strong>ActiveStorage Rollout</strong>.</p><h3>Hidden ActiveStorage (A)</h3><p>Keep everything from Paperclip as is, but also add ActiveStorage. We are going to use both of them at the same time. This means that during the time the attachments are migrated from Paperclip to ActiveStorage, if someone decides to upload an attachment, the user would still use the working Paperclip implementation (the same seamless flow the user is used to), but in the background, we would also duplicate the new attachment into ActiveStorage by making use of <a href="https://en.wikipedia.org/wiki/Observer_pattern">Observers</a>. (The user doesn’t need to know that 😁)</p><h3>Step ActiveStorageRollout (B)</h3><p>After the migration finishes, remove everything related to Paperclip and only use the new ActiveStorage implementation.</p><p>This logic makes sense to me, so let’s do it! (Easier said than done 😅)</p><p>Let’s move on to some coding 🎉.</p><h4>Step A.1. Install ActiveStorage</h4><p>We will start by installing ActiveStorage. Normally, Rails 5.2 already comes with it, so all we need to do is run:</p><pre>rails active_storage:install</pre><p>in the terminal.</p><p>This will generate the migrations to create the 2 tables mentioned above: ActiveStorageBlobs and ActiveStorageAttachments. Before migrating the tables, we add a new column called :storage_url of type string to the ActiveStorageBlobs table, so the final migration file looks like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/11b878a63d5d105e81506771d3648552/href">https://medium.com/media/11b878a63d5d105e81506771d3648552/href</a></iframe><p>The additional :storage_url column is used to store the direct URL of the attachment directly in the database. We will use this new column as a getter for the direct URL of the attachment.</p><p>Why? Because we can easily clone the database to any environment and still have working attachments. This means we don’t care about different storage configurations between environments, we know that the attachments work everywhere and new attachments will only be uploaded on the specific environment, but work on other environments if we clone the database where the attachment was uploaded. (Great if you are using multiple environments in your development workflow🥂).</p><p>The second reason is that we make use of direct SQL queries for some of our pages and this makes it easier to write them since the table contains the direct URL of the image to be used on the frontend. For example, we can create a query like so:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f3db0d8299458452a907edfb88d7f4c7/href">https://medium.com/media/f3db0d8299458452a907edfb88d7f4c7/href</a></iframe><p>or other variations. Discussing (materialized) views is out of the scope of this article, but we may discuss it in more detail in the future if you are interested 🥳.</p><h4>Step A.2. Install ActiveStorage validations</h4><p>Paperclip offers validations (example above) and we wanted something similar. Out of the box, ActiveStorage does not come with validations 😕, but we found an alternative: <a href="https://github.com/igorkasyanchuk/active_storage_validations">https://github.com/igorkasyanchuk/active_storage_validations</a></p><blockquote>If you are using active_storage gem and you want to add simple validations for it, like presence or content_type you need to write a custom validation method.</blockquote><blockquote>This gems doing it for you. Just use attached: true or content_type: &#39;image/png&#39; validation.</blockquote><p>Even though we will not be using the gem yet, we can move on by just installing it by adding it to our Gemfile and then running bundle install:</p><pre>gem &quot;active_storage_validations&quot;, &quot;~&gt; 0.6.1&quot;</pre><h4>Step A.3 Configure Cloud Storage Provider &amp; ActiveStorage</h4><p>In this step we will configure our cloud storage provider. Going into details for this specific task is beyond the scope of this blog post, so to sum it up, we can just configure a new bucket and permissions on AWS S3 (we are using AWS, but the process is similar for other storage providers) and add the necessary environment variables into your Rails project to allow access to your new AWS bucket.</p><p>For example, at <a href="https://www.sortlist.com/">Sortlist</a>, for Paperclip we were using AWS_S3_BUCKET_NAME, AWS_S3_REGION etc. In addition to these, for ActiveStorage we created AWS_AS_S3_BUCKET_NAME, AWS_AS_S3_REGION etc. As you might guess, AS refers to ActiveStorage so we know which keys are for which service 🔑.</p><p>In Rails 5.2 you should already have an app/config/storage.yml file. We want ActiveStorage to make use of the newly created AWS credentials, and this is the place to do it. After editing, the file should look similar to the following:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ee62146487e36e7f2e546c04a7f3f212/href">https://medium.com/media/ee62146487e36e7f2e546c04a7f3f212/href</a></iframe><p>We are not done yet 😒. We must tell Rails to use ActiveStorage, so we need to open the production.rb file and edit/add this line:</p><pre>config.active_storage.service = :amazon</pre><p>And finally we are finished with all the configurations involving ActiveStorage🤩.</p><h4>Step A.4 Migration Rake Task</h4><p>Most of the information/tutorials that we’ve found on the web do this directly in a Rails migration, but as discussed, this can be a long running action, so we moved it into a rake task:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4e20cbedbeae3f215c67781b22dfeec9/href">https://medium.com/media/4e20cbedbeae3f215c67781b22dfeec9/href</a></iframe><ul><li>There are multiple ways to execute this rake task, some of which include running it through background jobs with Sidekiq for example, or Ruby threads. At <a href="https://www.sortlist.com/">Sortlist</a>, an unwritten internal rule we try to abide by is that we don’t really want to fill Redis with these kinds of long running tasks, so we went with the second option: Ruby threads. As you may know, there are certain situations where Ruby threads can be used and this is one of them. Because going into detail for the above options is beyond the scope of this article and for the sake of simplicity, we will keep a simple version of the rake task. We can discuss it in a future article if you are interested 😉.</li><li>Firstly, we gather all the models (with the exception of abstract classes) that are used in our application into an array.</li><li>Secondly, we iterate over the array and check if the schema of the model contains a column_name matching the Regex containing file_name and if it does, we save them into an array. For example a model can have a logo_file_name, picture_file_name etc.</li><li>The next step is skipped if the model doesn’t have any columns that match the above example 👍🏼.</li><li>For the found columns we iterate over them and create an ActiveStorage record only if ActiveStorage does not contain that record. The reason for this is that if for some reason we cancel the rake task or it crashes, we can restart it and it will continue from where it left off, saving us time in the end (if you have hundreds of thousands of attachments).</li><li>The code for :duplicate_active_storage follows below and I will also explain the use of the @is_migration instance variable.</li></ul><h4>Step A.5 ActiveSupport Concern</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b251f4933395a4323bdd1d7bf781c217/href">https://medium.com/media/b251f4933395a4323bdd1d7bf781c217/href</a></iframe><ul><li>The duplicate_active_storage.rb concern is the one being used by the previous rake task to create the ActiveStorage attachment. This concern will also be included (include DuplicateActiveStorage) in the models that contain attachments (those models which are configured to use Paperclip).</li><li>In the previous rake task, we set the instance variable @is_migration to make sure that we don’t trigger an after_commit callback after creating the ActiveStorage attachment (which will result in an infinite loop 😱).</li><li>The duplicate_active_storage method gathers the attachment columns of the models for which we want to duplicate the Paperclip attachments into ActiveStorage, and for each one it creates the ActiveStorage attachment based on some conditions:</li></ul><p><strong>Conditions for creation:</strong></p><ul><li>If the record was updated, we check if the Paperclip attachment was updated. If it was updated, then we also update the ActiveStorage attachment. If the attachment wasn’t updated, we can skip all actions related to ActiveStorage. This happens in case of anafter_commit callback.</li><li>In case of an after_destroy callback, we check that the instance of the model was not deleted (we use the <a href="https://github.com/rubysherpas/paranoia">acts_as_paranoid</a> gem for soft deletion of records, hence the try(:deleted?) check, because we are paranoid 🤥). If the record was deleted, we will also remove it from ActiveStorage.</li><li>The rest of the logic of creating an ActiveStorage attachment is standard and is very similar to all the other tutorials on the web regarding this topic, but for completeness, I’ll briefly sum it up: so what is actually happening is that we are constructing the path for the direct URL of the attachment using the key method. This is actually the direct URL of the Paperclip attachment (the one from the old S3 bucket). We then pass on this direct URL to ActiveStorage, which will first download it and then upload it into the new configured S3 bucket. The downloading is necessary, because ActiveStorage does not know how to represent the attachment, so it needs to analyse it first. We then create the associated polymorphic ActiveStorage attachment record and we are good to go ☀️.</li></ul><h4>Step A.5 <strong>Update the storage_url of the ActiveStorage blob</strong></h4><p>After the attachment has been attached, we have to figure out what to do with the custom column that we created earlier (storage_url). The most intuitive way to update that column is to create a callback on the ActiveStorage::Blob model. We tried that first, but it seemed that nothing happened. After some research into the problem, we realised it was not easily achievable and we would have to monkey-patch 🐒 ActiveStorage (<a href="https://stackoverflow.com/questions/53226228/callback-for-active-storage-file-upload">source</a>).</p><p>Monkey-patching is generally a bad idea and is recommended only if you really know what you are doing. ActiveStorage is quite new and we are expecting a lot of improvements in the near future, so we decided against it.</p><p>But there is some good news ahead 😊. We were already making use of the <em>Observer pattern </em>quite a lot in our app so we gave it a try and we have a winner! 🍾 The result being:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3e82d4de2144ebbefc7162c84e341463/href">https://medium.com/media/3e82d4de2144ebbefc7162c84e341463/href</a></iframe><p>The observer pattern<em> </em>is implemented in our project by making use of the <a href="https://github.com/rails/rails-observers">Rails Observer gem</a>. More information can be found in the associated links.</p><p><strong>Hidden ActiveStorage (A) conclusion:</strong></p><p>With the above code in place, thanks to the concern that we’ve created, no matter which action is taken (create/update/destroy) through Paperclip, it will also be handled by ActiveStorage in the background. In addition, the ActiveSupport::Concern is used by the rake task to migrate the old attachments (in our case it took about 2–3 days which includes some minor fixes we had to do along the way). So basically we achieved DRY code and no downtime (our initial goal for this step of the process⚽️).</p><p>Let’s move on to <strong>ActiveStorage Rollout (B) 🎊</strong>.</p><h4>Step B.1 Remove Paperclip</h4><p>Now that the data is ready, thanks to the first step, we can completely remove Paperclip and rollout to ActiveStorage as the main and only way of storing attachments. This step is actually quite short and straightforward. The steps that we need to take here are:</p><ul><li>Remove Paperclip gem from the Gemfile and run a bundle install.</li><li>Remove any Paperclip configurations from production.rb / development.rb or other environments you may have.</li><li>Change any references of has_attached_file from your models to the ActiveStorage equivalent (e.g.has_attached_file becomeshas_one_attached etc.)</li><li>Make sure you update the model validations to make use of the new version from the active_storage_validations gem we have installed earlier.</li><li>Create a migration to drop all the Paperclip columns from your models. For example:</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/10e861e798d8898c61a7e8d55e518516/href">https://medium.com/media/10e861e798d8898c61a7e8d55e518516/href</a></iframe><ul><li>At this point we can also remove the rake task, the duplicate_active_storage.rb file and the includes DuplicateActiveStorage from the models in which it was being used.</li></ul><p>And this will conclude our journey regarding the migration from Paperclip to ActiveStorage. 🥳</p><p>Hopefully this made sense to you, but feel free to leave any comments if you think that some points may need further explanations or if anything is unclear.</p><h4>TL;DR 😇</h4><p><em>Interested in joining the product &amp; engineering team of a fast-growing tech start-up and you want to use top-notch technology? </em><a href="https://www.sortlist.com/"><em>Sortlist</em></a><em> is always looking for talented people to join us in Belgium or Romania. Check out our positions here:</em></p><p><a href="https://sortlist.breezy.hr/">Become our new colleague!</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/200/1*f3uSmbuWRWz8npGrKflDVQ.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4bffa2260e17" width="1" height="1" alt=""><hr><p><a href="https://medium.com/sortlist-engineering/migrating-from-paperclip-to-activestorage-a-different-approach-4bffa2260e17">Migrating from Paperclip to ActiveStorage: a different approach</a> was originally published in <a href="https://medium.com/sortlist-engineering">Sortlist Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>