<?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[Vestiaire Connected - Medium]]></title>
        <description><![CDATA[Vestiaire Collective’s Product &amp; Engineering blog. From AI to user research and software development, learn how product innovators and inspiring engineers design and build the leading global online marketplace for desirable pre-loved fashion. - Medium]]></description>
        <link>https://medium.com/vestiaire-connected?source=rss----c1311d187d2b---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Vestiaire Connected - Medium</title>
            <link>https://medium.com/vestiaire-connected?source=rss----c1311d187d2b---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 27 May 2026 23:36:19 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/vestiaire-connected" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Optimizing E2E Test Suites for Reliable Monolith Deployments at Vestiaire Collective]]></title>
            <link>https://medium.com/vestiaire-connected/optimizing-e2e-test-suites-for-reliable-monolith-deployments-at-vestiaire-collective-7c12433c4add?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/7c12433c4add</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[cypress]]></category>
            <category><![CDATA[qa]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[test-automation]]></category>
            <dc:creator><![CDATA[Clement Sehan]]></dc:creator>
            <pubDate>Tue, 21 Jan 2025 16:29:23 GMT</pubDate>
            <atom:updated>2025-01-21T16:29:23.216Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9nyNQLduBpuyM0W_RcuIbw.png" /></figure><h3>Introduction</h3><p>In the fast-paced world of fashion and tech, ensuring the reliability and smooth operation of critical applications is paramount. At Vestiaire Collective, our monolith application underpins essential features such as Login, Listing Form, and Checkout, which are vital to our business operations. With numerous engineers contributing to this repository, deploying changes can pose risks to the reliability of the entire system. To mitigate these risks and accelerate the deployment of smaller amounts of changes, we embarked on a journey to optimize our end-to-end (E2E) test suites.</p><h3>The Initial Shift to Cypress</h3><p>Our first step in this optimization journey was migrating our E2E tests to Cypress. Previously, we were using another tool that lacked integration with GitLab for running CI/CD test suites. This limitation created a dependency on our QA team to manually trigger the test suites and provide the go-ahead for deployments. As a result, our processes often faced delays due to timezone differences, with multiple teams located across different parts of the world. This move to Cypress was motivated by the need to integrate tests seamlessly into our continuous integration (CI) pipeline, distribute ownership of test suites beyond the QA team to all engineers, and ultimately shift towards a CI/CD (continuous integration and continuous delivery) shift-left development cycle. Cypress offered us robust testing capabilities, easy CI pipeline integration, and a shared responsibility model. However, this migration unveiled new challenges.</p><h3>Challenges: Execution Time and False Positives</h3><p>First of all, the initial challenge in migrating to Cypress was the fact that our QA team was not familiar with Cypress and JavaScript. This required efforts in training, establishing best practices and reviewing merge requests. However, our dynamic QA team at VC quickly adapted to these changes, demonstrating remarkable dedication and agility. In just 6–9 months, they successfully migrated over 700 test cases from the old solution to Cypress, showcasing their impressive ability to embrace and implement new technologies effectively.</p><p>Then, upon migrating to Cypress, we encountered two significant issues: extended execution times and numerous false-positive test failures due to test flakiness. The execution time for running regression tests before deployment expanded to around 45 minutes, which was unsustainable. Moreover, the flakiness of tests — leading to false positives — raised doubt about the reliability of our CI pipeline. Interestingly, we found that these two issues were intricately linked.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/806/1*EAVYYOyh4pogCGnvpljytA.png" /><figcaption><em>With an average of 11 regression runs per week, reducing execution time from 45mn to 20mn led us to the fantastic result of 4,5 hours saved per week on monolith deployment process.</em></figcaption></figure><h3>Addressing Execution Time Through Dynamic Waitings</h3><p>During our migration to Cypress, the primary focus was on decommissioning the old solution as quickly as possible, so code optimization wasn’t a priority at that time. As a result, one of the main reasons for our prolonged test execution times was the excessive use of static <strong>cy.wait()</strong> commands within our test code. A simple regex search in the codebase — <strong>cy\.wait\(\d+\)</strong> — revealed over 600 occurrences, leading to more than 2 million milliseconds (equivalent to 34 minutes) of unnecessary waiting time. This resulted in over 50% of our test execution time being spent on static waits. We identified two key strategies to address this:</p><p>1. <strong>Replacing Static Waits with Dynamic Assertions</strong>: By replacing <strong>cy.wait()</strong> with assertions such as <strong>should(‘be.visible’)</strong>, we could leverage Cypress’s built-in waiting mechanisms. Cypress inherently waits for elements to be in a ready state before continuing. This change helped in making our waiting time dynamic, effectively reducing waste and minimizing test flakiness.</p><p>2. <strong>Using API Synchronization</strong>: In many scenarios, the waiting time involves waiting for specific API call responses. In those cases, we implemented the usage of <strong>cy.intercept()</strong>. This allowed us to synchronize our tests with API call completions, ensuring the application state was ready before proceeding with subsequent test steps.</p><p>Before:</p><pre>myAppPage.submitAction.click();<br>cy.wait(3000);<br>myAppPage.confirmationButton.click();</pre><p>After:</p><pre>cy.intercept(&#39;POST&#39;, &#39;**/endpoint/*/action*&#39;).as(&#39;actionAlias&#39;);<br>myAppPage.submitAction.click();<br>cy.wait(&#39;@actionAlias&#39;).then(response =&gt; {<br>if (response.response.statusCode === 201) {<br>myAppPage.confirmationButton.click();<br>} else {<br>cy.fail(`Action failed, status code: ${response.response.statusCode}`);<br>      }<br>});</pre><p>In this example, we are replacing a 3-second static wait with a dynamic wait for a specific API response. This way, if the API takes less than 3 seconds to respond (which is very likely) the test will continue and will be faster. But if the API takes more than 3 seconds, it will still wait for the API (until the default timeout is set in Cypress configuration), preventing potential flakiness.</p><p>You might have noticed in this case there is no usage of an assertion such as <strong>.should(‘be.visible’)</strong>. It is not necessary because <strong>.click()</strong> is an “action command” in Cypress, which automatically checks the current state of the DOM and takes steps to ensure the element is “ready” for the action. For more details, refer to <a href="https://docs.cypress.io/app/core-concepts/interacting-with-elements">the Cypress documentation</a> on interacting with elements.</p><p>These improvements helped make our tests more resilient and efficient by adjusting waiting times dynamically, thus addressing flakiness caused by variations in application loading times.</p><p>It’s also important to highlight the outstanding work of the Vestiaire Collective frontend engineering team, who successfully migrated the entire web application to React. This transition significantly enhanced the application’s overall performance and stability, showcasing their technical expertise and dedication to continuous improvement.</p><h3>Reducing Flakiness and Enhancing Test Efficiency with APIs</h3><p>To further optimize our test suites, we focused on reducing unnecessary UI interactions. UI interactions are inherently slower and more prone to flakiness. For example, to test the checkout feature, we previously needed to perform several UI interactions — log in, access a product page, add an item to the cart — before reaching the checkout. While these interactions are critical for dedicated testing of individual features, for the checkout test itself, we could achieve the same state using API calls.</p><p>Example of Cypress custom command to handle add to cart by API:</p><pre>Cypress.Commands.add(&#39;addToCart&#39;, (itemIds, apiUrl, headersKey) =&gt; {<br>  cy.getCookie(headersKey).then(cookieValue =&gt; {<br>    const headers = {<br>      Authorization: `Bearer ${cookieValue.value}`<br>    };<br><br>    itemIds.forEach(itemId =&gt; {<br>      cy.request({<br>        url: `${apiUrl}/entities/current/items`,<br>        method: &#39;POST&#39;,<br>        headers,<br>        body: {<br>          itemId: itemId,<br>        },<br>        failOnStatusCode: false,<br>      }).then(response =&gt; {<br>        expect(response.status).to.eq(201);<br>      });<br>    });<br>  });<br>});</pre><p>If we apply the same approach for the other steps, then your test case can look like this:</p><pre>it(&#39;checks payment method availability in checkout&#39;, () =&gt; {<br>  cy.login(email, password);<br>  cy.setCookie(cookieValues);<br>  cy.cleanCart().addToCart([productId]);<br>  cy.visit(&#39;/checkout&#39;);<br>[...]<br>});</pre><p>In this example, you might have noticed that the test is cleaning the cart with <strong>cy.cleanCart()</strong> before performing the <strong>addToCart([productId])</strong> command. By cleaning the cart at the start, we ensure that each test begins with a clean slate, free from residual data that might distort results. This strategy aligns with a best practice in testing known as “state reset”, which emphasizes the importance of initializing or resetting data before executing test operations rather than cleaning up after the test.</p><p>Starting with a consistent and controlled state not only prevents unwanted test dependencies but also enhances test reliability by eliminating side effects caused by leftover data. Consequently, this ensures that our tests are both repeatable and dependable, providing accurate insights into the application’s behaviour.</p><p>To sum up, by performing API calls to log in and add items to the cart, we were able to bypass several layers of UI interactions and directly access the checkout page using <strong>cy.visit()</strong>. By applying this API-driven approach to several complex pathways within our product, particularly those occurring post-purchase, we reduced the execution time of some tests by over 60% and significantly diminished test flakiness.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ER2EIt7elop5CcopFMdJHw.png" /><figcaption><em>In May 2024, spec files’ median duration was 38 seconds and the slowest spec file had a median duration above 13 minutes.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6KlDhC4saGL6eTApuUb1iw.png" /><figcaption><em>In Nov 2024, spec files’ median duration was 13 seconds and the slowest spec file had a median duration of 4 minutes. In 6 months we’ve been able to decrease the spec file median duration by 66% and the slowest spec file duration dropped by 70%.</em></figcaption></figure><h3>Conclusion</h3><p>Through strategic optimization efforts, we have succeeded in bolstering the reliability and efficiency of our E2E test suites at Vestiaire Collective. By migrating to Cypress and addressing key pain points such as execution time and flakiness, we have reinforced trust in our deployment pipeline. Our focus on dynamic waiting mechanisms and API integration has not only streamlined the testing process but also paved the way for faster and more reliable deployments, ensuring that our monolith application continues to serve our business and customers effectively.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7c12433c4add" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/optimizing-e2e-test-suites-for-reliable-monolith-deployments-at-vestiaire-collective-7c12433c4add">Optimizing E2E Test Suites for Reliable Monolith Deployments at Vestiaire Collective</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Speed Up Image Background Removal Service With FastAPI and Triton Inference Server]]></title>
            <link>https://medium.com/vestiaire-connected/speed-up-image-background-removal-service-with-fastapi-and-triton-inference-server-3d3e6e722ba9?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/3d3e6e722ba9</guid>
            <category><![CDATA[triton]]></category>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[fastapi]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Tu Ta Quang]]></dc:creator>
            <pubDate>Tue, 10 Dec 2024 10:44:52 GMT</pubDate>
            <atom:updated>2024-12-10T10:44:52.519Z</atom:updated>
            <content:encoded><![CDATA[<h4>VESTIAIRE COLLECTIVE</h4><h4>Serving AI Models for Concurrent Requests at Scale</h4><h3>Introduction</h3><p>At Vestiaire Collective, we receive tens of thousands of product listings daily. To ensure a uniform and professional look highlighting products for buyers, we need to remove the background from each image.</p><p>Until now, this work has been done automatically by BARE, our in-house image background removal service.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qlcdyg3q8lqnBcFjZxOR9g.png" /><figcaption>Sneakers after being processed by BARE</figcaption></figure><p>We have been running the service on Vestiaire Collective’s Kubernetes clusters without GPU support. The core model behind BARE is a <a href="https://github.com/xuebinqin/U-2-Net">U2Net</a> deep learning model trained from scratch on Vestiaire Collective’s product images. (For details of the model implementation, see this <a href="https://medium.com/vestiaire-connected/how-to-create-your-own-background-removal-tool-849b25c70be0">post</a>).</p><h3>The Rise of Problems</h3><p>We focus on clipping the main image of each product. In total, we process around 25,000 images daily. To meet this demand with no GPU support, the first version of the BARE service scaled horizontally to at least 15 instances, each requiring a minimum of 2 CPUs and 8GB RAM and up to 4 CPUs and 16GB RAM. Despite this setup, resource requirements for what seems like a “simple” service were still high. The average response time of 2.5 to 3 seconds was acceptable for batch (offline) inferencing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*86mDlVrBgAAnu0RJ" /><figcaption>The number of requests increased 3 times from July 2024</figcaption></figure><p>However, demand increased when Vestiaire Collective’s internal pricing recommendation service began using BARE as a preprocessing step with stronger speed requirements. They used BARE to remove the background from seller-uploaded images at listing, in order to improve the quality of the product similarity search powering their pricing recommendations. BARE requirements rose to approximately 75,000 images per day (~3x increase).</p><p>Additionally, as the number of requests grew, we identified a memory leak in the service. Memory usage steadily increased over time, driving up infrastructure costs and negatively impacting the overall performance.</p><p>At this point, BARE’s processing time became a significant bottleneck. This prompted us to refactor the service to improve speed, enable real-time use cases, handle more concurrent clients, resolve the memory leak, and reduce reliance on horizontal scaling.</p><h3>Review of the Initial Deployment</h3><p>We reviewed BARE’s initial implementation to identify improvement areas. Here’s a recap of the first version’s setup:</p><p><strong>FastAPI</strong>: Used to expose the service via HTTP, with a single async endpoint for image background removal.</p><p><strong>Endpoint Tasks</strong>:</p><ul><li><strong>Pre-processing</strong>: Resizing images with numpy (CPU-bound).</li><li><strong>Model Inferencing</strong>: Predicting the foreground mask (CPU-bound).</li><li><strong>Post-processing</strong>: Resizing the mask to match the original image, clipping, and returning the masked image (CPU-bound).</li></ul><p><strong>Model</strong>: Served in ONNX format, using the OpenVINO backend, with no GPU support.</p><p>In code, the endpoint could be simplified as follows:</p><pre>import asyncio<br>import io<br>from fastapi import APIRouter, Depends, Response<br>from fastapi.responses import FileResponse<br>import app_models<br>from utils.request import read_image<br>from model.openvino_inference import BareInferenceOpenVINO<br><br>router = APIRouter()<br># loading u2net<br>bare_fast = BareInferenceOpenVINO()<br><br>@router.post(&quot;/clipping&quot;, response_class=FileResponse)<br>async def clipping(<br>    payload: app_models.PayloadInputU2NetFormData = Depends(<br>        app_models.PayloadInputU2NetFormData.as_form<br>    ),<br>):<br>    img_pil = read_image(payload.file)<br>    # preprocessing<br>    img_numpy = await asyncio.to_thread(bare_fast.preprocess, img_pil)<br>    # model inferencing<br>    pred = await asyncio.to_thread(bare_fast.predict, img_numpy)<br>    # postprocessing<br>    clipped_image = await asyncio.to_thread(<br>        bare_fast.post_process, pred=pred, img_pil=img_pil<br>    )<br>    # saving the image to a buffer, return it as a response<br>    buffer = io.BytesIO()<br>    clipped_image.save(<br>        buffer, format=&quot;JPEG&quot;, icc_profile=img_pil.info.get(&quot;icc_profile&quot;)<br>    )<br>    buffer.seek(0)<br>    return Response(content=buffer.getvalue(), media_type=&quot;image/jpg&quot;)</pre><p>We identified two primary issues with this design:</p><h4>1. Spawning Python Threads for CPU-Bound Tasks</h4><p>Each request dispatches subtasks to different threads from a pool of 40 (<a href="https://github.com/encode/starlette">starlette</a>’s default, on which FastAPI is based) as invested in this<a href="https://github.com/encode/starlette/issues/1724"> issue</a>. When concurrent requests exceed available threads, some requests must wait, creating an I/O bottleneck that occupies more RAM at high loads. While we could increase the thread pool, Python threading has limitations.</p><p><strong>Why isn’t Python threading efficient?</strong></p><p>Historically, Python threads don’t run in true parallel due to the Global Interpreter Lock (GIL), which prevents race conditions but limits parallelism for CPU-bound tasks. For more information on the GIL, see<a href="https://realpython.com/python-gil/#:~:text=The%20Python%20Global%20Interpreter%20Lock%20or%20GIL%2C%20in%20simple%20words,at%20any%20point%20in%20time."> What Is the Python Global Interpreter Lock (GIL)</a>.</p><p>Our endpoint tasks use numpy and OpenVINO, which are implemented in C/C++ and bypass the GIL. However, Python threading still doesn’t achieve true parallelism, highlighting an improvement area in our service.</p><h4>2. Violation of Separation of Concerns</h4><p>While FastAPI is excellent for API development, it’s not tailored for deep learning model serving. Our approach of serving the model directly in FastAPI complicates horizontal scaling.</p><p>For example, imagine a scenario where a service requires 1GB of RAM (primarily for background tasks like logging) and 4 CPUs (mainly for deep learning model inferencing). To ensure availability, we deploy two replicas. If a high load pushes a replica to 80% RAM usage, another replica is created, even if CPU usage is only at 50%. This results in inefficiency, as we now have three replicas, each using 4 CPUs, but primarily to handle memory demands.</p><p>If we had separated concerns from the start — offloading the model serving to a dedicated platform — we could scale the hardware for background tasks independently, keeping the hardware requirements for model serving constant.</p><h3>The Refactor</h3><p>With these issues in mind, we opted to implement a dedicated model-serving server. This offloads the CPU-intensive model inferencing from FastAPI, restores FastAPI’s async benefits, and simplifies horizontal scaling to serve more clients efficiently.</p><p>On AWS, CPU instances are generally much cheaper than GPU instances. However, we added GPU support for model inferencing with the idea that a single expensive GPU can deliver more efficient performance than multiple inexpensive CPUs at the same total cost.</p><h4>Why Triton Inference Server?</h4><p>We chose NVIDIA’s<a href="https://developer.nvidia.com/triton-inference-server"> Triton Inference Server</a> for the model serving part due to several advantages:</p><ul><li><strong>Queuing and Batching</strong>: Triton efficiently queues and batches requests, boosting throughput, especially under heavy load.</li><li><strong>Multi-framework Support</strong>: Supports diverse frameworks (e.g., PyTorch, TensorFlow, ONNX), adding flexibility.</li><li><strong>Scalability and Load Management</strong>: Dedicated platforms like Triton simplify horizontal scaling and provide load balancing.</li><li><strong>Performance Optimization</strong>: Triton, optimized for NVIDIA GPUs, accelerates inference for intensive models like U2Net.</li></ul><p>For setup details, please take a look at<a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/user_guide/model_configuration.html"> NVIDIA documentation</a>. Triton requires a config file defining parameters like batching time and input/output formats. For BARE, the config looks like this:</p><pre>platform: &quot;tensorrt_plan&quot;<br>max_batch_size: 4<br>instance_group [<br>    {<br>      count: 1<br>      kind: KIND_GPU<br>      gpus: [0]<br>    }<br>  ]<br>dynamic_batching {<br>    max_queue_delay_microseconds: 1000<br>}<br>input [<br>{<br>  name: &quot;input&quot;<br>  data_type: TYPE_FP32<br>  dims: [ 3, 320, 320 ]<br>}<br>]<br>output [<br>{<br>  name: &quot;output&quot;<br>  data_type: TYPE_FP32<br>  dims: [ -1, -1, -1 ]<br>}<br>]</pre><p>We used a single GPU and one instance of the model, as this setup was sufficient for our needs. If we increase the number of model instances, Triton will spawn processes to serve them when using <a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/python_backend/README.html">Python backend</a> or threads when using TensorRT backend (our case).</p><p>We set “max_queue_delay_microseconds: 1000”, meaning that requests arriving within the same millisecond are grouped into a batch for inference. The model was converted to TensorRT for deployment using the TensorRT engine (platform: “tensorrt_plan”) to maximize the performance of the NVIDIA GPU.</p><p>In FastAPI, only minimal modifications were required. The key change was introducing an asynchronous call to the Triton server via a client library:</p><pre># import libraries<br><br>triton_client = new_client(<br>        config.TRITON_URI,<br>        config.TRITON_SSL,<br>        verbose=False,<br>        use_grpc=config.TRITON_USE_GRPC,<br>    )<br>@router.post(&quot;/clipping&quot;, response_class=FileResponse)<br>async def clipping(<br>    payload: app_models.PayloadInputU2NetFormData = Depends(<br>        app_models.PayloadInputU2NetFormData.as_form<br>    ),<br>):<br>    # preprocessing are kept as before<br>    # model inferencing<br>    triton_inputs = triton_client.prepare_triton_input_img(img_numpy)<br>    pred = await triton_client.predict_mask(<br>        triton_inputs, model_name=config.TRITON_BARE_MODEL<br>    )<br>    # postprocessing are kept as before</pre><h3>Questions Raised in the Refactor</h3><p>At this point, you may be wondering why the preprocessing and postprocessing steps are still handled in FastAPI, given that they are CPU-bound tasks and could potentially cause blocking.</p><p>We kept the processing (resize) part in FastAPI to ensure all calls to Triton use the same input shape, thereby leveraging batching. Consequently, the postprocessing also needs to remain in the FastAPI server.</p><p>If you’re a fan of Triton, another question might come to mind: Why not move all processing to Triton, since it supports Python backend? For us, doing so would essentially turn Triton into another version of the FastAPI server, but with queuing support. Additionally, we would have to transfer the input image in its original size (on average 1.5 MB) from FastAPI to Triton, which is time-consuming.</p><h3>Results</h3><h4>Improvement in Latency</h4><p>After the refactor, our service became significantly leaner and approximately 15x faster than before, with the capacity to handle more requests. To highlight the difference clearly, the figure below shows the latency data logged by Grafana from July 1, 2024, to the end of August 2024.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5O3A6oEPhfb7IR0l" /><figcaption>Service latency before and after the refactor</figcaption></figure><p>We deployed the first version of the refactor on July 21. This version introduced a new API endpoint with the improvements while keeping the old endpoint to give clients time to switch completely. After the initial deployment, clients were still using the old API endpoint with the model being hosted directly on the FastAPI server, so although there was a noticeable drop in latency, it wasn’t very significant.</p><p>The steadily increasing latency trend during this period was attributed to the previously mentioned memory leak. On August 5, all clients switched to the new endpoint. At that point, latency dropped significantly and stabilized consistently.</p><h4>Reducing the Cost of Infrastructure</h4><p>We were also able to divide the number of replicas, memory, and CPU usage by three with the addition of 2 GPUs (actually the service needs only 1 GPU, 2 is to ensure high availability during <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/">Kubernetes Pod eviction</a> scheduled by the platform team).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pjUlV39lC5dIoXED" /><figcaption>CPU &amp; RAM cost for BARE before refactoring</figcaption></figure><p>The average daily cost for operating the old version of BARE is $78 ($56 for CPU (in green) and $22 for RAM (in blue)).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oQF0hpAzQ8BBlKGY" /><figcaption>CPU &amp; RAM &amp; GPU cost for BARE after refactoring</figcaption></figure><p>After the refactor, the daily cost is $60 ($14 for CPU, $11 for RAM, and $35 for GPU (in yellow)), corresponding to a reduction of 23% compared to the previous version.</p><h4>Memory Leak Mitigation</h4><p>Another interesting point we discovered is that thanks to moving the model inference task to Triton, the memory leak no longer occurs in our FastAPI server. This leads us to suspect that the memory leak was caused by OpenVINO and ONNX.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*M4Vc62IXFysGDy_J" /><figcaption>Memory leak before and after the refactor</figcaption></figure><p>In the memory usage chart, before switching all requests to the new endpoint, the replicas’ memory usage was increasing over time. Once the limit (16GB) was reached, Kubernetes redeployed the replicas, thus clearing the memory as shown by the drops on the chart. After switching to the new endpoint, memory usage still dropped, but not because the limit was reached — rather, it was due to the Kubernetes Pod eviction.</p><h3>Recap</h3><p>Here’s a concise recap comparing the previous and new implementations of BARE, highlighting the issues in the initial setup and the improvements achieved through the refactor:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/940/1*s3Zs8w1M4qRA4pk-TotV2Q.png" /></figure><h3>Conclusion</h3><p>This refactor allowed us to handle higher traffic and resource demands, serving multiple concurrent clients with 15x faster inference speed, increasing overall efficiency while reducing the operation cost by 23%.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3d3e6e722ba9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/speed-up-image-background-removal-service-with-fastapi-and-triton-inference-server-3d3e6e722ba9">Speed Up Image Background Removal Service With FastAPI and Triton Inference Server</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</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 create your own AI Chat Moderation model]]></title>
            <link>https://medium.com/vestiaire-connected/how-to-create-your-own-ai-chat-moderation-model-c2bab9029c65?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/c2bab9029c65</guid>
            <category><![CDATA[fashion]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[chat-moderation]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Aurélien Houdbert]]></dc:creator>
            <pubDate>Tue, 02 Jul 2024 13:17:08 GMT</pubDate>
            <atom:updated>2024-07-03T09:07:19.118Z</atom:updated>
            <content:encoded><![CDATA[<h4>Vestiaire Collective</h4><h4>Lessons learned from building a chat message classifier internally</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/873/1*XfEx4bdCLJId6athefC_LA.png" /></figure><h3><strong>1. Introduction — Why monitoring and moderating your platform’s chat is crucial</strong></h3><p>On Vestiaire Collective, buyers and sellers can discuss through a chat interface to get more information about products, negotiate prices, clarify item conditions, and agree on sales. This direct communication enables more personal and efficient transactions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/358/0*VWTWNtaAjh_j7UbS.png" /></figure><p>However, this chat feature is also an open door for scammers or users trying to avoid platform fees. Despite our platform’s security and protection measures, some users still try to exchange private information to finalize deals in person or on less secure platforms.</p><ul><li><strong>Scammers</strong> negatively impact user experience, resulting in decreased trust in our platform.</li><li><strong>Circumvention</strong> directly affects GMV (revenue) and Cost Per Order by shifting transactions outside the platform.</li><li><strong>Toxic behavior</strong> significantly degrades user experience and engagement.</li></ul><h4>Challenges</h4><p>Automatically blocking messages and banning users from the chat raises several challenges:</p><ul><li>Our AI model must achieve a balance of recall and precision, capturing a sufficient proportion of unwanted messages while accurately identifying when a message should be flagged. <strong>Incorrectly banning users can lead to poor user experience and decreased engagement</strong>.</li><li><strong>Scam messages and circumvention attempts require different banning processes</strong>. Scammers should be identified quickly and permanently banned, while legitimate users attempting to trade outside our platform should undergo an educational process with progressive banning measures.</li></ul><h3>2. Chat message classification — A short review of available solutions</h3><p>There are various approaches you can use to classify text.</p><h4>Regex</h4><p>Regex, or pattern matching, is often the easiest way to classify text. It involves defining a set of prohibited words/patterns and creating regex rules around them. However, developing a comprehensive list of patterns can be time-consuming and is not robust enough to counteract sophisticated scammers. Regex also lacks semantic understanding, leading to misinterpretations.</p><p>For example, if your regex rules include the word “Instagram,” you will not be able to differentiate between:</p><blockquote><em>Circumvention attempt</em>: “Do you have Instagram ?”<br><em>Legit information</em>: “I bought it 2 years ago from someone I met on Instagram.”</blockquote><p>In our case, the first message intends to move the discussion to Instagram, while the second message only provides information on the item’s origin.</p><h4>Classic Machine Learning</h4><p>Machine Learning is a powerful tool for text classification. Most traditional ML techniques utilize word counts and co-occurrence methods, such as TF-IDF (Term Frequency-Inverse Document Frequency). Common algorithms include<a href="https://scikit-learn.org/stable/modules/naive_bayes.html"> Naive Bayes</a>,<a href="https://scikit-learn.org/stable/modules/svm.html"> Support Vector Machines</a> (SVM), and<a href="https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html"> Logistic Regression</a>. In many cases, traditional machine learning can achieve performance comparable to larger deep learning models, especially when datasets are not very large or complex.</p><p>However, they face three main challenges in the context of chat message moderation:</p><ol><li><strong>Scammers’ pattern adaptability</strong>: Scammers can quickly adapt their language and strategies, rendering your model’s vocabulary and features obsolete.</li><li><strong>Multilingual setting</strong>: In a multilingual environment, the vocabulary can grow rapidly, resulting in very large embeddings. This can lead to increased computational resources and complexities in model management.</li><li><strong>Broader context comprehension</strong>: The vocabulary size can significantly increase when n-grams are used to include more “word groups.”</li></ol><h4><strong>Deep Learning LLMs</strong></h4><p>Large Language Models (LLMs) such as<a href="https://huggingface.co/docs/transformers/main/en/model_doc/bert"> BERT</a> excel in text classification tasks due to their <a href="https://jalammar.github.io/illustrated-transformer/">transformer-based architecture</a>, which captures text semantics and nuances.</p><p>These models are relatively easy to use and fine-tune using the t<em>ransformers</em> library from HuggingFace. HuggingFace provides pre-trained models and user-friendly tools to customize them for your application.</p><p>However, there are several considerations to keep in mind when using deep learning models:</p><ol><li><strong>Computational Resources</strong>: Training, fine-tuning, and deploying BERT or other deep learning models can be resource-intensive.</li><li><strong>Data Requirements</strong>: Deep learning models often require large amounts of labeled training data to achieve optimal performance. Acquiring and labeling this data can be time-consuming and expensive.</li><li><strong>Interpretability</strong>: Deep learning models, especially those based on transformers, do not easily provide insight into which features are used to make decisions, which can be an issue in applications requiring high levels of transparency.</li></ol><p>Fine-tuning pre-trained models can help tackle the two first points. Indeed, you will need much less data and much less computing resources. Using the HuggingFace model repository, you can find open models pre-trained on general tasks or tasks similar to yours.</p><h3><strong>3. Data collection — How we built a dataset out of poorly labeled data</strong></h3><p>Vestiaire Collective historically used a regex moderation system to identify suspicious messages, which were then manually reviewed by human annotators. This process resulted in a dataset of manually verified messages, providing an <strong>excellent source of information for the model to understand semantic nuances in messages</strong>.</p><p><strong>But there were still two main issues:</strong></p><ol><li>This data only represents messages flagged by regex, <strong>leaving gaps in unflagged patterns.</strong></li><li>Human <strong>labelers are not 100% accurate</strong>.</li></ol><h4>Heuristics</h4><p>To tackle the first issue, we came up with various heuristics to enhance our dataset with safe circumvention and scam messages from our dataset of chat messages sent on the app.</p><p>For instance, one heuristic we employ to identify scam messages involves analyzing the number of line breaks, messages in the channel, and account age in days.</p><p>We have implemented similar heuristics for phone numbers and safe message identification, among others.</p><h4>Message relabeling</h4><p>Because human labelers are not 100% accurate at classifying messages, they create inaccuracies that result in less stable training and lower performance.</p><p>To improve labeling accuracy, we tested self-training and clustering/majority voting techniques.</p><h4>LLM relabeling</h4><p>When exploring data relabeling solutions, we also tried to use the latest LLMs to label our dataset. Generative AI LLMs have strong semantic understanding capabilities. With a little prompt engineering, it is possible to describe our moderation rules and chat guidelines to the model.</p><p>In our experiments, we used private models such as <a href="https://openai.com/index/chatgpt/">ChatGPT</a> (3.5 and above) from OpenAI and <a href="https://www.anthropic.com/">Claude</a> (version 1 and above) from Anthropic, in addition to trying open-source models such as Mistral (Mixtral-8x7b, Mistral-7b) and Llama 2.</p><p>These models have strong semantic understanding capabilities but fail to understand the intent behind a single message. To reuse the same example — “Do you have Instagram?” — most of these models fail to understand the real underlying intention, which is to move the discussion outside the platform.</p><p>To improve performance, we tried various prompting strategies:</p><ul><li><strong>Few shot example prompting</strong>: providing a few examples of correct classification with expected output format.</li><li><strong>Reasoning strategy</strong>: provide a reasoning framework to force the model to explain the message content and interpret the intent before providing a definitive label.</li></ul><p>These methods improved the raw performance of the model but weren’t good enough to relabel our entire dataset.</p><p>We also fine-tuned small open source models (7B and 13B versions of Llama 2 and Mistral) on a small sample of curated messages and labels. With fine-tuning, we tried to teach the model our moderation guidelines and some reasoning strategies. It worked and the model learned our rules and reasoning strategy, but it still could not understand underlying intents.</p><p>Even though the results are not satisfying yet, this work is still ongoing.</p><h4>Contextualization of messages</h4><p>The first versions of our model were performing inference at the message level. This strategy works well but sometimes lacks the context of previous messages. For example, the message “<em>33</em>” could be an answer to a user inquiring about the size of the item, but it could also be the first part of a phone number sent over multiple messages (+33 is the French country code).</p><blockquote><em>🙍‍♀️</em>: “<em>Hey, what is the size?”<br>🙎‍♂️</em>: “<em>33”</em></blockquote><blockquote><em>🙎‍♂️</em>: “<em>Here is my</em>”<br><em>🙎‍♂️</em>: “<em>number</em>”<br><em>🙎‍♂️</em>: “<em>33</em>”<br><em>🙎‍♂️</em>: “06~”<br><em>🙎‍♂️</em>: “ 82”<br>…</blockquote><p>Such a model can work on messages concatenated with a few past messages, but the performance is not great because messages with their context are a lot longer than single messages. The first version of the model had not been trained on such message lengths and often got lost with too much information in large context messages.</p><h4><strong>Re-building labeled messages within entire conversations</strong></h4><p>To achieve great performance for both individual messages and messages within their context (past messages), we needed to rethink our dataset. We redefined our heuristics to identify conversations where all messages are safe.</p><p>For conversations with unsafe messages, we ideally need perfect labels for all messages in the context. This allows us to create data batches with varying context lengths, helping the model understand what makes a conversation unsafe.</p><p>However, as the dataset size increases, this requirement becomes less critical since larger datasets naturally capture more nuances and variations.</p><h4>Data processing</h4><p>We mentioned that using LLM models and tokenizers from HuggingFace requires very few pre-processing steps.</p><p>However, some special characters might not be handled correctly by the tokenizer and be attributed an [UNK] unknown token (a default token for elements/words/subwords not available in the tokenizer’s vocabulary). <strong>This is typically the case for emojis 😀</strong>. If your pre-processing doesn’t handle emojis correctly, scammers might be able to <strong>communicate information through emojis</strong>.</p><blockquote>“0️⃣6️⃣4️⃣2️⃣…” and “🔥🔥🔥🔥…” will both be tokenized as “[UNK] [UNK] [UNK] [UNK]…” making it extremely difficult to correctly predict the message label.</blockquote><p>But if you convert emojis to text before tokenization, you will end up with a far better emoji representation in your model.</p><blockquote>“0️⃣6️⃣4️⃣2️⃣…” is converted to “:zero: :six: :four: :two: …” which will give a precise tokenized sentence.</blockquote><p>As part of the tokenizer choice, you can also experiment with the <em>cased</em> (sensitive to case) or <em>uncased</em> (all characters are lowered, accents are removed, etc.) versions. If your use case involves only English text classification you might want to head towards an <em>uncased</em> tokenizer, whereas in a multilingual setup, a <em>cased</em> tokenizer is better suited to keep all accents and specificities of languages.</p><h3>4. BERT — Efficient text classification using Transformers Architecture</h3><h4>BERT</h4><p>For text classification tasks, you can find many architectures and pre-training open source. We chose a pre-trained BERT mostly because it was trained on multilingual data (&gt; 100 languages) and had different tokenizers available.</p><p>The BERT model we use is a relatively small model of 179 million parameters which requires only 700 MB to fit into memory. Although you need <strong>only one small GPU</strong> for <strong>efficient fine-tuning</strong>, this model can be <strong>deployed on a CPU</strong> and still guarantee a short <strong>response time</strong>. In our case, the 95th percentile response time is below 100ms. In comparison, a 70 billion parameter LLM (such as Llama 3 or Mistral 70b versions) would require 260 GB to fit into memory.</p><p>The very first version of our model was a binary classifier. The poor initial data quality led to merging circumvention and scam labels into one unique category.</p><p>Performance was great at this point, but we needed to differentiate between a scam message and a circumvention attempt.</p><h4>BERT multi-class</h4><p>Distinguishing between scam messages and circumvention attempts enables targeted blocking and banning procedures. Ideally, soft bans can be implemented for legitimate users unaware of chat guidelines, while scammers should be subject to a more stringent hard ban procedure.</p><p>Therefore, the next versions of our classifier were trained on multi-class data and refined using the methods detailed in previous sections.</p><p>We observed a slight performance decrease when distinguishing between circumvention and scam messages, likely due to the semantic similarity between both classes. The model struggles to differentiate between the two categories, leading to lower confidence in each class.</p><h4>BERT + ML classifier</h4><p>Through the two previous versions of our model, we noticed that binary classification yielded better results but failed to distinguish between circumvention attempts and scam messages. To address this limitation, we incorporated a CatBoost classifier to predict the likelihood of a message being a scam. This approach leverages categorical features such as account age, sender type, and purchase history to improve our model’s accuracy.</p><h3>5. Model training — Technical details</h3><h4>Batch creation</h4><p>Given a set of conversations, how can we create training samples? We want the model to train on single messages and messages within context. To illustrate our sampling process, let’s use this example conversation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/445/1*_B2kmBx_9ar5Zj7OVFyxBw.png" /></figure><p>“<em>Are you on Instagram?</em>” is a circumvention attempt. The user is trying to move the conversation to Instagram to continue negotiating prices and avoid platform fees.</p><p>From this conversation, we can create various data samples:</p><ul><li><strong>Single message with positive label</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/450/1*ungoehhQ1GjfcNjLY3tEPA.png" /></figure><ul><li><strong>Message with context with positive label</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/442/1*s1GvNU4CeYEJlt6D_a6yLQ.png" /></figure><ul><li><strong>Message with context with negative label</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/443/1*ZiQBOByiiCdk9EYDAeIf1Q.png" /></figure><p>From one single conversation, we can create multiple training data samples of various lengths, various numbers of messages included in context, etc. By doing so, your model will learn to deal with varying message lengths and understand what makes a set of messages or conversations unsafe.</p><p>This sampling process also highlights the need for clean labels at the message level. As previously mentioned, having labels for every message in a conversation is more beneficial than having a dataset with labels on individual messages sampled from different conversations.</p><p>If you have a large amount of data, it will naturally capture more nuances and variations without needing such a sampling strategy.</p><h4>Data augmentation</h4><p>Data augmentation in NLP tasks is less straightforward than for computer vision tasks. The sampling process we use can already be viewed as a dataset augmentation technique.</p><p>Chat messages are often misspelled, either by inattention or because scammers are deliberately misspelling words to try and bypass the model. Based on this observation, we came up with three augmentation techniques:</p><ul><li><strong>Random character deletion: </strong>randomly removing characters in messages. This will break words or tokens, forcing invariance on word misspelling.</li><li><strong>Random character insertion: </strong>randomly inserting characters in messages. This will break words or tokens, forcing invariance on word misspelling.</li><li><strong>Digit replacement: </strong>replacing all digits in a message will introduce invariance regarding phone number, prices, sizes, etc.</li></ul><h4>AWS training job and hardware choice</h4><p>To train our BERT model, we use an AWS SageMaker training job with<a href="https://huggingface.co/docs/sagemaker/en/train"> Hugging Face estimator</a> on a <em>g4dn</em> GPU instance. The model is small enough to fit on the NVIDIA T4 GPU. The model itself requires as little as 700Mb of RAM to fit on the GPU. The limitation will come from the maximum length of inputs and batch size. In our case, training with a maximum length of 512 tokens (upper bound input length of BERT model), we are limited to a batch size of 12 to fit in GPU memory.</p><p>We trained the model for 3 epochs using the AdamW optimizer with linear learning rate decay starting from lambda=1e-5, batch size of 12 with 4 steps of gradient accumulation (larger batches don’t fit on a T4 GPU), weight decay of 3e-4 for regularization and a few warmup steps. We use default values for Beta1, Beta2, and epsilon for the AdamW optimizer.</p><h3><strong>6. Evaluation — How do we compare models?</strong></h3><p>Evaluation is a tricky process.</p><h4>Training evaluation</h4><p>We use a test set to measure classification metrics such as precision, recall, F1 score, AUC, etc. However, the ground truth labels in our test set are not 100% accurate, which introduces noise and makes it difficult to compare models. Due to these inaccuracies, the differences in metrics between models may be smaller than what is required to achieve statistical significance. This means that any observed differences could be attributed to the noise from mislabeled test data rather than true performance differences, making the comparison statistically meaningless.</p><h4>Inference evaluation</h4><p>To evaluate model performance in production, we follow metrics such as precision and recall but also customer contacts or the number of users targeted by bad messages.</p><p>We achieve this by using human labelers who review a sample of predictions of the model.</p><h3>7. Serving — How to serve this model in production?</h3><p>Even though the model needs to be trained on GPU, we can perform inference on CPU for real-time moderation (with a 95th percentile below 100ms).</p><p>Our API, written in Python FastAPI, is served on Kubernetes for optimal service scaling and cost optimization.</p><h3>8. Conclusion</h3><p>Chat moderation is crucial to prevent GMV loss due to platform circumvention and improve user experience.</p><p>Data is often the main driver of success. More data and granular labels can truly unlock great performances. In our case, data collection, processing, and relabeling were our biggest challenges.</p><p>Through continuous refinement and evaluation, our AI-powered moderation system can adapt to evolving threats, maintaining a secure and engaging environment for all users.</p><p>Even though BERT is not the latest LLM model out there, it is a better fit for our use case: smaller, faster, bidirectional encoder, etc. Large GenAI models can be a good solution to get started on a subject but keep in mind that there are many other great models out there designed specifically for your use case.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c2bab9029c65" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/how-to-create-your-own-ai-chat-moderation-model-c2bab9029c65">How to create your own AI Chat Moderation model</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our Android Application Meets Jetpack Compose]]></title>
            <link>https://medium.com/vestiaire-connected/our-android-application-meets-jetpack-compose-507633471174?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/507633471174</guid>
            <category><![CDATA[jetpack-compose]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[design-systems]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Efe Ejemudaro]]></dc:creator>
            <pubDate>Mon, 29 Jan 2024 14:15:30 GMT</pubDate>
            <atom:updated>2024-01-29T14:15:30.604Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TBsaQy6IGbSA_x4U" /><figcaption><a href="https://unsplash.com/photos/white-block-toy-lot-TaEd6ndkRWM">Photo by Elodie Oudot on Unsplash</a></figcaption></figure><p>As with every new technology, it’s always a good idea to introduce it pragmatically into a codebase that’s live in production and has millions of users; trying to ensure unnecessary problems aren’t introduced and understanding the benefits that come with this new tool. For us on the Android team at Vestiaire Collective, Jetpack Compose was one of these tools. So even though there was a lot of buzz around it and it was being actively pushed by many, here’s how we went about it.</p><p>For a long time in Native Android development, User Interfaces (UI) were built using Extensible Markup Language (XML). Views and ViewGroups are represented using XML tags, and the UI is fully developed as a tree of these tags in XML documents and inflated into Activities or Fragments, actively updating each view’s state wherever and whenever necessary.</p><p>But in 2021, Jetpack Compose was released; a brand new declarative methodology for building User Interfaces in Android. Compose has been actively gaining buzz in the Android community, making its way into more and more applications on the Google Play Store.<a href="https://developer.android.com/jetpack/compose/why-adopt"> Some advantages it provides over XML are:</a></p><ul><li>Less code to achieve the same UI; leading to fewer bugs and faster development time.</li><li>More intuitive to write as Compose utilises a declarative API.</li><li>Compose is interoperable with XML. Meaning you can have both of them together in the same screen and codebase, enabling a progressive migration for Compose into projects.</li><li>It’s also powerful in terms of what can be achieved. Creating complex animations has never been easier.</li></ul><h3>Learning Compose as a team using the official Codelabs</h3><p>With Compose comes a significant shift in thinking about code, methodology and architecture, amongst other things.<a href="https://codelabs.developers.google.com/?cat=Android&amp;product=android&amp;text=compose"> The official Android documentation has some codelabs that provide a great path for getting on-boarded into Jetpack Compose,</a> from the basic concepts, such as creating a text or a button in Compose and arranging composables in groups to the more complex ones like state management, Side Effects, and UI Testing. In the Android team, we began our Compose journey by going through these codelabs together as a team. These group sessions, while taking a time slot of an hour a week, brought really good results; keeping learning motivation high and relying on one another’s understanding for the confusing part of the new concepts. Altogether, these codelabs gave us a foundation to build on; a solid understanding of the ideas Compose brought forward.</p><p>For these sessions, we used the Mob Programming Format, which consisted of three major kinds of roles rotated weekly among the team members. First we have the “driver’’, one team member assigned to be the active developer in that session. They will be the one that actively codes, sharing their screen for everyone to follow. Then we have the ‘navigator’, a second team member that guides and directs the driver, looking for resources on what to do and organising what step of the code needs to be addressed next. And lastly, we have the “facilitators”, which are the other team members, following the resources and validating what’s being done by the driver and the navigator. It’s almost as if we are in a car, going on a journey.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*RiVi3y-vJEZ6n1Dj" /><figcaption><em>Graphic highlighting the Mob Programming Format</em></figcaption></figure><p>Afterwards, taking advantage of the momentum from the codelabs, we decided to migrate one of our existing screens on the Android application to Jetpack Compose, using the same format. The ideal screen would be one that has low risk on the business side and could do with a polish user experience-wise. These criterias landed us on the user profile screen. Revamping this screen enabled us to think about Compose out of the guides of the codelabs, but actually using it to create a user interface directly related to our project. Of course we did face a number of challenges but nothing too difficult to figure out as a team. Eventually, with these sessions, we gained some valuable experience and confidence to create UI with Jetpack Compose.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/423/0*vrvIqm-CByld3YIj" /><figcaption><em>Profile screen migration on Vestiaire Android app</em></figcaption></figure><h3>Powering our Design System with Jetpack Compose</h3><p>Jetpack Compose itself can be seen as a ‘design system’. What do I mean by that? Compose, as we interact with it as developers, is basically components (called composables). These components can be grouped together to make a desired UI and can be themed with a base theme providing colours and fonts. The Product Design Team at Vestiaire Collective has been working on unifying screens on the app and with Jetpack Compose came an opportunity to introduce a design system for Vestiaire Collective’s mobile applications. A design system ensures uniformity, as it provides everything from basic tokens such as colours, shapes, and fonts to patterns likeList of Product Blocks and Error Screens. As such, our design system, named Accent, was introduced. Accent provides all components that can be used on the Android application going forward, providing the tokens and also providing the actual components and patterns built from these tokens. The design system provides consistency and is uniform across all Vestiaire Collective platforms: Android app, iOS app and Web app. To learn more about our Accent design system,<a href="https://www.youtube.com/watch?v=KF3murFG5-8"> here’s a talk from Rami Trabelsi describing it and showing how it was developed and introduced into the Android application</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*w01lV4YEvhRAmsU1" /><figcaption><em>Accent design system showcasing its tokens, components, patterns, and blocks</em></figcaption></figure><p>How is it related here? Well on the Android app, Accent was created and driven using Jetpack Compose. All atoms, colours, dimensions, and typography were created in the base theme used on all new screens called AccentTheme, providing uniformity and maintainability with minimal lines of code on the feature itself. This also means any change made in Accent takes effect across the entire app instantly. Powerful, isn’t it?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KHEkuo-c4DioDXqc" /><figcaption><em>Code snippet showcasing the use of Accent</em></figcaption></figure><h3>Investigation into Compose performance</h3><p>As with every new technology, especially one as big as this that would make some changes to the very way we think about code, we wanted to make sure to have as much information as we could before introducing it to the application. Keeping this in mind, the Core Mobile team did a lot of investigation around a lot of topics in Jetpack Compose. Introducing guidelines for a lot of Compose concepts, such as recomposition count, performance, managing state effectively, navigation among composables, and of course, relying on and contributing to the design system.</p><p>To also ensure adding Compose didn’t introduce sudden spikes to metrics such as project build time and application size, we added Compose dependencies to the Android codebase and revamped a screen only used internally to get a measure of these metrics: our feature flag manager screen.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/398/0*9oLVgo27ABAb8GzX" /><figcaption><em>Revamped feature flag manager screen</em></figcaption></figure><h3>And some features…</h3><p>After all the preliminary work that has been done, of course we have to introduce Jetpack Compose to the codebase at some point. Finally in the first quarter of 2023, the Hero Product Detail Page was introduced and Compose and the Accent design system were deemed sufficiently ready to work on this feature with. Therefore, we went with it. On a personal note, this was some of the most fun I’ve had working on a new feature.</p><p>And yes, there have been more and more features using the Accent design system and Jetpack Compose through the entire year. To name just a few, we have the Pick Up Location and Product Slider components on the Homepage, and the Notification Centre.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CQD_0ggMO4OaGonI" /><figcaption><em>Graphic showcasing the use of Jetpack Compose in the application</em></figcaption></figure><h3>Some pitfalls we encountered</h3><p>While the good has undoubtedly outweighed the bad, we have also encountered some issues with Compose in our relatively short stint. Compose is still quite immature as it has been out for just about two years, and that means there are quite a number of functionalities that are not yet developed. For some context, its counterpart, XML, is over a decade old and you can imagine the stability that comes with that.</p><p>Some of the issues we have encountered are:</p><ul><li>Implementing Pull to Refresh on the new Notification Centre Revamp as Compose Material 3 artifact does not have that functionality yet.</li><li>Achieving a sticky scrolling behaviour on the profile screen’s tabs while using Compose interoperability with XML.</li><li>Some lag while testing using development builds. (This does not happen on our release builds.)</li><li>Consistent issues with composables previews used while screens are under development.</li></ul><p>However, we’ve been able to find our way around these pitfalls and overall, it has been a good experience so far and it should get even better in the future with Compose and Accent design system getting more mature.</p><h3>Conclusion</h3><p>From my experience so far, I would say it has been a resounding success for us to take the step to introduce Jetpack Compose to our codebase. And due to more features using Jetpack Compose, the Accent design system has also gotten more and more mature, leading to faster development time as new features can take advantage of components already developed in Accent. Currently our most used component is AccentText with exactly 150 usages, proving scalable so far, with the added advantage of only needing to change a configuration in exactly one place if ever needed. Progressively introducing Compose has also proven to be a very smart decision as we have been able to swiftly mitigate any problems we faced in production keeping the precious stability of our application.</p><p>Thanks for reading.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=507633471174" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/our-android-application-meets-jetpack-compose-507633471174">Our Android Application Meets Jetpack Compose</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</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 create your own background removal tool ?]]></title>
            <link>https://medium.com/vestiaire-connected/how-to-create-your-own-background-removal-tool-849b25c70be0?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/849b25c70be0</guid>
            <category><![CDATA[fashion]]></category>
            <category><![CDATA[background-removal]]></category>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Aurélien Houdbert]]></dc:creator>
            <pubDate>Tue, 19 Sep 2023 13:31:09 GMT</pubDate>
            <atom:updated>2023-09-19T13:31:09.368Z</atom:updated>
            <content:encoded><![CDATA[<h4>Vestiaire Collective</h4><h3>How to create your own AI background removal tool?</h3><h4>Lessons learned from building a clipping engine internally</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*d5cw1zILwU87IjO1" /><figcaption>Photo by <a href="https://unsplash.com/@sharegrid?utm_source=medium&amp;utm_medium=referral">ShareGrid</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Introduction — Why you should clip images</h3><p>Images are the heart of <a href="https://fr.vestiairecollective.com/">Vestiaire Collective</a>. Thanks to images, potential buyers are able to assess the quality, color and condition of products. This makes image quality and consistency critical to user engagement and conversion rate.</p><p>When browsing our apps and website, you may have noticed that all main item pictures have no background on Vestiaire Collective, thanks to a third-party background removal tool to clip them. Some advantages of using a background removal tool include:</p><ul><li>Luxury <strong>look and feel</strong> of the platform</li><li>The background becomes <strong>less distracting</strong></li><li><strong>Consistency</strong> &amp; better <strong>image quality</strong></li><li>Better <strong>assessment of the color and quality of the product</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DPM7NsNSM2GzOJhR" /><figcaption><em>Products for sale on Vestiaire Collective’s website</em></figcaption></figure><p>While there are many existing providers in the market, building your own tool can save costs and provide a customized solution. This is what inspired us to explore how we could create our own background removal deep learning model. In this article, we will share our experience with building a background removal tool at Vestiaire Collective, including the challenges we faced and the solutions we found.</p><h3>Understanding the basics of background removal — A quick review of available solutions</h3><p>Removing the background from an image may seem like a simple task but it’s actually quite challenging. The background of an image can be composed of different objects, textures, and colors, and it can be difficult to distinguish between the background and the foreground objects (even for human eyes).</p><p>Background removal is a highly researched subject in AI and recently gained attention with the rise of deep learning models. We can distinguish two different types of approaches: Instance Segmentation and Salient Object Detection (SOD).</p><h4><strong>Instance Segmentation</strong></h4><p>Is a technique that specializes in identifying and labeling objects in an image, while also segmenting them from the background. Mask-RCNN is a good example of an instance segmentation network.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/649/1*5d_TC-iMzg_3iQk2hGFvYQ.png" /><figcaption>Mask-RCNN prediction example</figcaption></figure><h4><strong>Salient Object Detection (SOD)</strong></h4><p>Is a technique that identifies the most visually significant object(s) in an image and separates them from the background. The goal of Salient Object Detection is to highlight the most important parts of an image, which are typically the objects or regions that draw the viewer’s attention. UNet or U2Net are great examples of SOD networks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/457/1*SXseVqai72tHMMnh2II3Yw.png" /><figcaption>U2Net prediction example</figcaption></figure><p>After a quick review of state of the art models, we identified U2Net models as the most effective approach for our use case. Indeed, U2Net (a SOD model) only needs precise segmentation masks and doesn’t require an object label to segment images. SOD were designed to accurately delineate foreground from background, contrary to Instance Segmentation that is optimized to locate the object in the image. Also, we wanted a model as general and as flexible as possible, and one that is able to handle unseen labels during training.</p><h3>Collecting the data — Data pre-processing is your best friend</h3><p>As mentioned in the previous section, in order to train U2Net properly, we need to build a dataset of images paired with their corresponding segmentation masks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uwOJmOvNtAkhZUAJ6KGUNQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TDBy0AOh6FdVM974jHMx3w.png" /><figcaption>Original image and its corresponding segmentation mask</figcaption></figure><p><strong>Good news</strong>: Vestiaire Collective has been clipping images for years with the help of a third party provider. This means we have access to an almost unlimited source of images. 🎉</p><p><strong>Bad news: </strong>At Vestiaire Collective, all images are stored in JPEG format, clipped images are cropped, centered and saved with a white background.</p><p>So why is that an issue ?</p><ul><li>The white background makes it difficult to extract a clean segmentation mask from the clipped images. If you try to set the white pixels as background, you may end up with “holes” in the object you are trying to extract if it contains white parts.</li><li>Because clipped images were cropped and centered, they are no longer aligned with the original image. During the training of U2Net, we need pixels between mask and image to be perfectly aligned.</li><li>JPEG images are compressed versions of original images, which will result in poor mask quality around edges.</li></ul><p>To solve these issues we had to go through two main pre-processing steps:</p><h4><a href="https://medium.com/towards-data-science/image-stitching-using-opencv-817779c86a83"><strong>Image Stitching</strong></a></h4><p>Is the process of combining multiple overlapping images to create a single, wider image. This is achieved by identifying common features in the images and aligning them to create a seamless, panoramic view. In our case, the clipped image was directly extracted from the original picture so this technique works pretty well to realign images.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/557/0*TS3fUZ_AUHiP7En5" /><figcaption>Image stitching</figcaption></figure><h4><strong>Mask Refinement</strong></h4><p>Is used to remove artifacts arising because of the white background removal and the JPEG format. To remove these artifacts, we use a<a href="https://docs.opencv.org/4.x/d4/d13/tutorial_py_filtering.html"> smoothing technique</a> (Gaussian blur) combined with<a href="https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html"> morphological transforms</a> (combination of dilation and erosion).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MwubFZSLXiGkHdrFw8t4XQ.png" /><figcaption>Segmentation mask artifacts</figcaption></figure><p>These techniques proved to be efficient but were not sufficient to recreate blindly a clean dataset. About 1/3 of images were of poor quality, with “white holes” too large to be corrected by the mask refinement step. In the end we still needed to review our dataset manually.</p><p><strong>Thanks to these methods we were able to build a 5K images dataset!</strong></p><p><em>Notes: </em>Later in the project, we identified that this mask refinement issue was not scalable as we needed more data to reach even better performances. Our final dataset contains only PNG images coming from our manual clipping provider here at Vestiaire. <strong><em>This first 5K JPEG images dataset provided very encouraging results that gave us traction with stakeholders and helped kick-start the project.</em></strong></p><p>Also, even if our final dataset was built using a different source of data (png format with no background), we still used the stitching method to realign clipped images with the originals.</p><h3>Building the model — U2Net</h3><p>As mentioned earlier, the model we selected is U2Net. Their paper and code can be found here:</p><p><a href="https://github.com/xuebinqin/U-2-Net">GitHub - xuebinqin/U-2-Net: The code for our newly accepted paper in Pattern Recognition 2020: &quot;U^2-Net: Going Deeper with Nested U-Structure for Salient Object Detection.&quot;</a></p><p>We followed the implementation of the paper and used the exact same training settings. We trained the model from scratch on a dataset of 18,000 hand curated images.</p><p>When building the model, we made several general observations:</p><ul><li>Better dataset quality led to better results than bigger dataset of lesser quality. Of course more data of better quality would result in even greater performances!</li><li>The model is “only” <strong>40 million parameters</strong>. The pre-trained model, provided by the research team, was trained on a dataset of 10,000 images. Retraining from scratch is a viable option if you have enough training data. It is particularly useful if your custom dataset is significantly different from the pre-trained dataset (which was our case because our data is fashion items and the pre-training data is<a href="https://paperswithcode.com/dataset/duts"> DUTS-TR</a>).<br><em>e.g. In our case, we wanted to remove human body parts from images, but the pre-trained dataset included numerous examples of humans treated as foreground. When we fine-tuned the pre-trained model on our custom dataset, it resulted in undetermined regions, making the background removal less accurate.</em></li></ul><h3>Evaluating the model — A tricky task</h3><p>At Vestiaire Collective, all images are manually reviewed and clipped images are visually checked by a human. A given proportion of images that are clipped by our third-party provider don’t pass this manual check. Our goal with this project is to be at least as good as the current system, but more economical.</p><p>So here comes the difficult part of the project. Background removal quality strongly relies on visual criteria of the clipping and it is very difficult to find a metric that reflects perfectly for the current rejection rate. Usual quality metrics such as f1-score, Dice coefficient, or IoU are not always sufficient to assess the overall quality of the model. The human rejection rate doesn’t correlate well with these classic segmentation metrics.</p><p>A much more useful metric we use is the “relax-f1”, a metric used in the original paper of U2Net. This metric is nothing more than a f1-score assessed only on the edges of the clipped object. This metric is particularly efficient because the visual quality of a clipping mostly comes from the quality of the edges.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oo6M45uP1ly0sxvBMk-LYw.png" /><figcaption>relax-f1 visualisation</figcaption></figure><p>In order to assess the production performance of the model and compare it against our third-party provider, we use a dataset of 1,000 images manually reviewed by our curation agents. This process is long and costly and prevents quick iterations.</p><p>The metrics such as relax-f1 were a powerful way to carefully tune and select the best models to be sent for manual performance evaluation.</p><h3>Post-processing to enhance and refine model predictions</h3><p>The model choice, data quality and training strategy are very important but in the end, what differentiates a bad clipping from a good one is the post-processing.</p><blockquote>The most important thing to understand here is that all quality metrics that we discussed in the previous section will not be impacted by our post-processing step. This means that even if those metrics indicate good results, there’s still a chance that the output may not look visually appealing, which we are trying to solve with our post-processing strategy.</blockquote><p>To understand post-processing, it’s helpful to understand that the model (in our case, U2Net) outputs a probability map for each pixel in a low dimension (320 x 320px). When upscaling this map to the original image’s dimensions, even minor errors and inconsistencies can become more apparent and visually disturbing. For example, a blurry region in the predicted foreground can appear much larger when upscaled, leading to poor visual quality, particularly around the edges of the clipping.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kmvVnjgvp9Fi6q7T38lX1A.png" /><figcaption>Upscaling blurriness effect on the segmentation prediction</figcaption></figure><p>The edges are regions of uncertainty with a smooth gradient between 0 and 1. This is expected as it is the region delimiting the background from the foreground. This is a natural consequence of the model’s attempt to delineate the background from the foreground. In our example, we can also notice a blurry region at the bottom of the shoe which can lead to unwanted effects if left unaddressed.</p><p>To resolve this, we can try to binarize the map to get rid of these blurry areas (notice how the bottom of the sole was corrected).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tVW38MEfdlZgNEKB-SvrJQ.png" /><figcaption>Binarization of the segmentation prediction</figcaption></figure><p>However, this method may not be entirely sufficient. Although the edges are now better defined, the smooth transition between the background and foreground has been lost, resulting in a stair-step effect that is less visually appealing.</p><p>We need to take two additional steps to address this issue: blurring the mask and stretching it. The blurring will reintroduce a smooth transition between the background and foreground. However, this approach can result in too much blurring, which can further degrade the image. To overcome this problem, we can use a linear stretching step to reduce the blurring radius and achieve smooth, steep, visually appealing edges.</p><h3>Results and Lessons Learned</h3><p>Our background removal tool has achieved an impressive level of performance, better than our current third-party provider and significantly reducing costs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NGYX9rsFVt1t_4nW4Frzqg.gif" /><figcaption>Clipping engine results on fashion items</figcaption></figure><p>One of the biggest challenges we faced during this project was obtaining sufficient quantities of high-quality data. We spent a huge amount of time searching for suitable data sources in our database. However, even with limited available data, we were able to jump-start the project, generating interest from stakeholders. The traction with stakeholders then enabled us to access more and better quality data, unlocking budgets and resources.</p><h3>Final considerations</h3><p>Building our own background removal tool internally helped us drastically reduce image curation cost. In this project, data was our key to success.</p><p>Although the model itself (U2Net) greatly impacts the clipping quality of your tool, keep in mind that post-processing must not be ignored as it may help you get perfect results.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=849b25c70be0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/how-to-create-your-own-background-removal-tool-849b25c70be0">How to create your own background removal tool ?</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[SEO Case Study: Migrating Tradesy to Vestiaire Collective in 6 Months]]></title>
            <link>https://medium.com/vestiaire-connected/a-comprehensive-case-study-migrating-tradesy-com-to-us-vestiairecollective-com-in-6-months-9432e70c326e?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/9432e70c326e</guid>
            <category><![CDATA[seo]]></category>
            <category><![CDATA[mapping]]></category>
            <category><![CDATA[redirection]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[technical-seo]]></category>
            <dc:creator><![CDATA[Jean-Eric Blas-Châtelain]]></dc:creator>
            <pubDate>Tue, 02 May 2023 15:49:05 GMT</pubDate>
            <atom:updated>2023-05-09T08:19:48.947Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_baUTLFZJqHDLBt-XI3kag.jpeg" /><figcaption>SEO migration | Vestiaire Collective</figcaption></figure><p>As businesses grow and evolve, website migrations become crucial for expansion and enhanced user experiences. In March 2022, Vestiaire Collective acquired Tradesy, its American counterpart. As a leading online marketplace specializing in pre-owned luxury fashion items growing rapidly in the US, migrating the Tradesy website to the Vestiaire Collective website smoothly and in a timely manner was critical both for our new and experienced users.</p><p>Mission accomplished! Thanks to incredible team efforts, we successfully completed the soft migration of tradesy.com to us.vestiairecollective.com in only six months. This in-depth article outlines the sophisticated, data-driven techniques and strategies employed to ensure a smooth and efficient migration, ultimately leading to significant improvements in keyword rankings, traffic, and overall website performance.</p><h3>Data-Driven Techniques and Strategies</h3><ol><li><strong>Leveraging Search Console Data for URL Analysis</strong><br>The first step in the process was to identify all the URLs on tradesy.com that had received at least two clicks in the past 16 months. This information was extracted from Google Search Console, a tool that helps website owners monitor and optimize their search performance. With this data in hand, the team could then focus on redirecting these high-performing URLs to their corresponding pages on us.vestiairecollective.com.</li></ol><p><strong>2. Deleting Duplicate Products</strong><br>To ensure a seamless transition, all duplicate products on Tradesy were deleted, as they would be redirected 1-to-1 by the IT team. This step helped to avoid any confusion or duplicate content issues during the migration process.</p><p><strong>3. Competitor Benchmarking with SEMrush<br></strong>SEMrush, an SEO tool that provides data on competitor performance, was used to benchmark the performance of Tradesy and Vestiaire Collective. This analysis helped the team to identify gaps and areas of opportunity in terms of keywords and SEO strategies.</p><p><strong>4. Matching URLs Using Search Console and Scraping APIs<br></strong>A combination of Search Console data and web scraping APIs was used to identify the best Vestiaire Collective URL for each Tradesy URL. This process involved finding the keyword that drove the most clicks for each Tradesy URL and then searching site:us.vestiairecollective.com {Tradesy keyword} to find the most relevant page on the new domain.</p><p><strong>5. Manual 1-to-1 Matching for High-Performing URLs<br></strong>For the remaining URLs that generated significant traffic, the team performed a manual 1-to-1 matching process. This ensured that the most relevant and high-performing pages were accurately redirected to their new counterparts on Vestiaire Collective’s domain.</p><p><strong>6. Breadcrumb Scraping for Category Matching<br></strong>To further refine the matching process, the team used web scraping techniques to extract the last breadcrumb element from each Tradesy URL. This information was then used to match Tradesy’s categories with those on Vestiaire Collective, ensuring a smooth user experience and preserving SEO value.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mT1oYm_YoKHvAUddH1vMKg.png" /><figcaption>Categories | Vestiaire Collective website</figcaption></figure><p><strong>7. Redirecting Unmatched Brands and Closet URLs to the Homepage<br></strong>For brands not available on Vestiaire Collective and unmatched closet URLs, the team chose to redirect these pages to the homepage of us.vestiairecollective.com. This strategy helped to maintain a positive user experience and preserve some of the SEO value from these pages.</p><p><strong>8. Catalog Page Matching and Product Page Redirects<br></strong>The team successfully matched over 30,000 catalog pages between the two websites, ensuring that users could easily find the products they were looking for on the new domain. For product pages, the team implemented redirect rules based on product IDs, with special consideration for products that were migrated by the sellers themselves. This meticulous approach ensured that legacy Tradesy URLs were taken into account during the migration process.</p><h3>Results Achieved</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GqQZdbrvyMb_A7gwwmw3hw.png" /><figcaption>Migration announcement on the website</figcaption></figure><p>The successful migration of Tradesy.com to us.vestiairecollective.com yielded impressive results, demonstrating the effectiveness of the detailed and data-driven techniques employed:</p><ul><li><strong>Catalog Page Matching</strong>: A total of 30,000 catalog pages were matched and redirected, ensuring a seamless user experience while minimizing any potential traffic loss.</li><li><strong>Product Page Redirection</strong>: ID-based rules were implemented to redirect product pages, including specific tactics for products that were migrated by the sellers themselves. This approach ensured that visitors could easily access the desired items on the new site.</li><li><strong>Legacy URL Consideration:</strong> The migration plan took into account legacy URLs from Tradesy, ensuring that these older links were also redirected and maintained their value in terms of search rankings and user experience.</li><li><strong>Keyword Growth: </strong>The total number of keywords targeted by the website doubled, reflecting a more comprehensive and robust SEO strategy that catered to a wider range of search queries.</li><li><strong>Improved Ranking Positions:</strong> The number of keywords ranking in the top 3 positions (1–3) experienced substantial growth and doubled as well. This improvement indicates enhanced visibility in search results, leading to higher click-through rates and increased organic traffic.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mke8BDlKqxEVoGgIGkzHkA.png" /><figcaption>TOP 3 trends</figcaption></figure><ul><li><strong>Large-scale URL Redirection:</strong> A total of 800K URLs were redirected throughout the migration process, showcasing the vast scope and meticulous planning involved in successfully executing such an extensive migration project.</li><li><strong>Advanced Tracking:</strong> To better understand the user journey and identify visitors searching for Tradesy in Google, tracking was implemented to pinpoint users who landed on us.vestiairecollective.com after searching for Tradesy-related terms. This information provided valuable insights into user behavior and allowed for further optimization of the website experience for these visitors.</li><li><strong>User Engagement and Retention:</strong> The migration resulted in improved user engagement and retention rates, as users searching for Tradesy were seamlessly redirected to the corresponding pages on us.vestiairecollective.com. This positively impacted key engagement metrics such as bounce rate, time on site, and pages per session.</li></ul><p>These results testify to the effectiveness of the data-driven techniques, meticulous planning, and rigorous execution employed throughout the migration process. Speaking of which, let’s dive deep into the key steps of our global migration plan in the next section.</p><h3>Detailed Migration Plan: 8 Major Steps</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wzrv_kg5vupfsJOCY7Cniw.png" /><figcaption>Social media announcement</figcaption></figure><ol><li><strong>Preparatory Measures:</strong> Integration and verification of Tradesy’s Search Console, IP whitelisting to facilitate website crawling, obtaining admin access for Google Analytics, and conducting an in-depth analysis of Tradesy’s SEO templates and tools.</li><li><strong>Current Situation Evaluation:</strong> Benchmarking hard and soft KPIs (search analytics and indexed pages), assessing link profiles (including toxicity cleanup), generating a migration SEO checklist, and establishing a precise migration timeline.</li><li><strong>Redirection Planning:</strong> Crafting a detailed mapping strategy by page type based on crawl results, evaluating external link structures, identifying and rectifying crawling errors and additional redirect requirements, refining redirection spreadsheets, and validating redirection rules in collaboration with the development team, followed by a thorough final review with DevOps.</li><li><strong>Deployment Phase:</strong> Uploading the carefully crafted redirection plan (to remain live for at least one year post-migration) and executing the post-migration SEO checklist.</li><li><strong>Tracking Implementation:</strong> Updating Search Console and Google Analytics with the change of address features, settings adjustments, and ensuring thorough documentation.</li><li><strong>Communication Strategy:</strong> Incorporating the former name in title tags and meta descriptions, featuring it in the website footer, designing interstitials for redirected users, and updating social media account handles and descriptions.</li><li><strong>Promotion and Outreach:</strong> Orchestrating email announcements, public relations campaigns, guest posts, social media engagement, Pay-per-click (PPC) advertising, and updating LinkedIn profiles.</li><li><strong>Monitoring and Adjustment:</strong> Establishing a dedicated dashboard to track the migration’s impact and make data-driven adjustments as needed.</li></ol><h3><strong>Conclusion</strong></h3><p>Vestiaire Collective’s successful soft migration of Tradesy.com to us.vestiairecollective.com exemplifies the power of meticulous planning, data-driven execution, and thorough monitoring in achieving a seamless transition. Leveraging data, employing multiple techniques, and adhering to a comprehensive migration plan enabled the company to achieve remarkable improvements in keyword rankings, traffic, and overall website performance. This case study provides valuable insights and serves as a model for effectively managing large-scale website migrations in the e-commerce sector, particularly when dealing with complex, multi-faceted websites in the luxury fashion space.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9432e70c326e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/a-comprehensive-case-study-migrating-tradesy-com-to-us-vestiairecollective-com-in-6-months-9432e70c326e">SEO Case Study: Migrating Tradesy to Vestiaire Collective in 6 Months</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Balancing Innovation and Scalability: Developing Vestiaire Collective’s Photo Library Feature on…]]></title>
            <link>https://medium.com/vestiaire-connected/balancing-innovation-and-scalability-developing-vestiaire-collectives-photo-library-feature-on-f3d28cbf42fe?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/f3d28cbf42fe</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[experience]]></category>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[self-improvement]]></category>
            <category><![CDATA[apps]]></category>
            <dc:creator><![CDATA[Ali Fakih]]></dc:creator>
            <pubDate>Wed, 19 Apr 2023 09:53:41 GMT</pubDate>
            <atom:updated>2023-04-19T10:21:31.585Z</atom:updated>
            <content:encoded><![CDATA[<h3>Balancing Innovation and Scalability: Developing Vestiaire Collective’s Photo Library Feature on iOS</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/764/1*87It9I93X_O_a1XiATivaw.png" /><figcaption>Apple Photos and Vestiaire Collective</figcaption></figure><p>As an experienced iOS engineer who has navigated the fast-paced, iterative world of agile development, you know that creating a feature with intricate technicalities can be a daunting task. Whether it’s utilizing native libraries or third-party tools, the learning curve can be steep. I want to take you on a journey through the development of the Photo Library feature at Vestiaire Collective and share the strategies that I found to be effective. Remember, there’s no one-size-fits-all solution, but hopefully, my experience will provide valuable insights and inspiration for your next project. Let’s dive in!</p><h4>RFC</h4><p>An essential aspect of any successful iOS development team is the creation and adherence to an RFC (Request for Comments) process. But what exactly is an RFC and why is it so crucial? An RFC is a document that outlines the proposed feature, its impact on users, and the technical and architectural plan for its development. The term originated from the days of ARPANET (Advanced Research Projects Agency Network), where researchers would present ideas for discussion and feedback from their peers. In the team standards, it is a requirement that an RFC is created and approved before development can begin. This may seem like an added step that could potentially slow down the development process, but it serves a critical purpose. It ensures that all necessary research and planning have been done. It also helps to identify and address potential issues before they become costly and time-consuming problems down the line.</p><h4>RFC to the Rescue: How to Avoid Common Pitfalls in iOS Development</h4><p>When a developer — let’s call them “A” — is tasked with creating a new feature, they may jump straight into coding without conducting proper research. This leads to a flurry of questions and feedback from the other iOS engineers on the team during the review process. This constant back-and-forth can be incredibly disruptive to the developer’s focus and attention and can leave them feeling like they’re dancing on eggshells. Furthermore, if these changes end up altering the core of the initial plan, it can lead to two types of bugs: those that are hard to detect during testing due to time constraints, and those that slow down the overall progress of the feature’s development. This can be a risky and nerve-wracking experience for the developer.</p><p>With a little reflection, it becomes clear that the time spent on creating an RFC may seem like a wasted effort in the beginning, but it is actually a valuable investment in terms of onboarding the team, sharing ideas, and receiving feedback. It’s important to remember that this is just one example of the many benefits that an RFC can provide.</p><blockquote><em>“None of us is as smart as all of us.” — Ken Blanchard</em></blockquote><h4>Coding Time</h4><h4>First steps with the Photos framework on iOS</h4><p>Now let’s go on a journey through the process of developing a photo library. To begin, let’s review the fundamental methods of fetching photos. One way to retrieve photos from the iOS native photo library is by utilizing the <strong>photos framework </strong>provided by Apple. This framework grants access to the user’s photos and videos, enabling various operations such as retrieving metadata and editing them.</p><p>Here’s an example of how to fetch all photos from the user’s library:</p><ol><li>Import the Photos framework in your ViewController:</li></ol><pre>import Photos</pre><p>2. Create a PHFetchOptions object to specify the options for fetching the photos. For example, you can set the sort descriptor to sort the photos by date:</p><pre>let fetchOptions = PHFetchOptions()<br>fetchOptions.sortDescriptors = [NSSortDescriptor(key: &quot;creationDate&quot;, ascending: false)]</pre><p>3. Use the PHFetchResult object to fetch the photos from the library:</p><pre>let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)</pre><p>4. Iterate through the fetchResult object to access each photo:</p><pre>fetchResult.enumerateObjects { (asset, _, _) in<br>    // do something with the asset, for example, retrieve the image by calling the following function<br>    let image = getAssetThumbnail(asset: asset)<br>}</pre><p>5. To retrieve the image from PHAsset, you can use the following function:</p><pre>func getAssetThumbnail(asset: PHAsset) -&gt; UIImage {<br>    let manager = PHImageManager.default()<br>    let option = PHImageRequestOptions()<br>    var thumbnail = UIImage()<br>    option.isSynchronous = true<br>    manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFit, options: option, resultHandler: {(result, info)-&gt;Void in<br>        thumbnail = result!<br>    })<br>    return thumbnail<br>}</pre><blockquote>Note: You should check for the user’s authorization before accessing their photo library.</blockquote><p>Straightforward right? It’s a good place to start.</p><h4>Defining the architecture of your photo library</h4><p>With these steps in mind, you need to think about refactoring it and scaling it so it fits the project architecture. Here, you will need to brainstorm your ideas and try to explain every thought to yourself to make sure that you’ve found the right way forward. You will need to convert your thoughts into an architectural approach that you can explain to your team.</p><p>Here’s my architectural plan after reading the Apple documentation and researching existing solutions:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mdBLhnJ71CM1JrU1" /><figcaption>UML Diagram</figcaption></figure><p>In this diagram, you can see a new way of implementing the functional <a href="https://developer.apple.com/documentation/photokit/phasset">PHAsset</a> and <a href="https://developer.apple.com/documentation/photokit/phassetcollection">PHAssetCollection</a>.</p><blockquote>Note: I will not dive deep into writing the code.</blockquote><p>In the bottom left, the MediaPickerCoordinator is where the navigation is handled (check the <a href="https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps">coordinator pattern</a>). Inside the coordinator, we initiate two properties:</p><ol><li>MediaManager</li><li>LibraryDataLogic</li></ol><p>The MediaManager is similar to a functional repository that will handle your requests to fetch PHAsset , PHAssetCollection or PHAsset from a specific PHAssetCollection.</p><p>On the bottom right of the diagram, the MediaManager holds two kinds of repositories:</p><p>1. PhotoMediaRepository</p><p>2. AlbumMediaRepository</p><p>Each repository is responsible for providing the requested data and passing them to the MediaManager to convert them to a friendly model so that the UI can consume it.</p><h4>PhotoMediaRepository</h4><p>Three functions will be contained by this class:</p><p>1. loadLibrary</p><p>2. getPhotoPaginated</p><p>3. getFirstMedia</p><p>loadLibrary is responsible for returning the number of assets that exist in the library and retrieving PHFetchResult&lt;PHAsset&gt; which is the result of all the assets that do exist in the library, and that is only because we had a different approach previously — this will be deleted after full release — .</p><p>getPhotoPaginated, as per its name, will return a paginated MediaProtocol. MediaProtocol is a type wrapper of the PHAsset.</p><p>getFirstMedia will return the first media from a PHAssetResult.</p><h4>AlbumMediaRepository</h4><p>This class contains one main function named loadCollections and will return an array of MediaCollectionProtocol type (which is a type wrapper of PHAssetCollection).</p><h4>Building the LibraryMediaManager</h4><p>Now that we have these two repositories, we can start building the LibraryMediaManager which will feed us with serval properties and let us take action upon album selection.</p><p>Here are the provided functions:</p><p>1. <strong>func</strong> loadLibrary()-&gt;Int</p><p>2. <strong>func</strong> loadMedia(from collection: MediaCollectionProtocol)-&gt;Int</p><p>3. <strong>func</strong> getPhotoPaginated&lt;T:MediaProtocol&gt;(pagination:LibraryMediaPagination) <strong>throws</strong> -&gt;[T]</p><p>4. <strong>func</strong> getFirstMedia&lt;T:MediaProtocol&gt;(from mediaRsult: PHAssetResult) <strong>throws</strong> -&gt; T</p><p>5. <strong>func</strong> loadCollections&lt;T:MediaCollectionProtocol&gt;(collectionType:MediaCollection.CollectionType) -&gt; [T]</p><p>6. <strong>func</strong> getRecentCollection&lt;T:MediaCollectionProtocol&gt;() <strong>throws</strong> -&gt; T</p><p>Some delegates are also available to refresh the results of library changes.</p><p>These functions are called from a DataLogic class that communicates with the ViewModel accordingly. Additionally, the MediaManager also has delegates to refresh the results on library changes.</p><p>This architecture allows for a separation of concerns with the following split:</p><ul><li>The MediaManager handles the communication with the photo library.</li><li>The repositories handle the specific data fetching.</li><li>The DataLogic and ViewModel handle the application logic and presentation of the data.</li></ul><p>This makes the code more organized, maintainable, and easy to test.</p><p>This design also allows for flexibility in the future, as new features or changes to the photo library can be easily implemented in the MediaManager and repositories without affecting the rest of the codebase.</p><p>By using a functional repository pattern, the MediaManager can be reused across different parts of the app. It also allows for easy testing as the MediaManager and repositories can be tested independently of the rest of the codebase.</p><p>In summary, the MediaManager is designed to handle the communication with the photo library, the PhotoMediaRepository and AlbumMediaRepository handle the specific data fetching, and the DataLogic and ViewModel handle the application logic and presentation of the data. This design allows for a separation of concerns, flexibility, and maintainability in the codebase.</p><h4>Testing and troobleshooting</h4><p>With this architectural setup, all our use cases were succeeding without any issues. Surprisingly however, one problem popped up at the last moment before release.</p><blockquote><em>It’s important to keep in mind that even with a solid architecture, bugs may still appear, but having a good structure in place can make it easier to track down and fix them. Additionally, keeping in mind the user experience, accessibility, and security will help you deliver a better product.</em></blockquote><p>During testing, we primarily utilized test devices but occasionally used personal devices. We encountered no performance issues as the number of media on these devices did not exceed 4,000 photos. However, when a teammate tested the feature on their device containing 70,000 photos, it became clear that there were <strong>significant performance issues</strong>, specifically that the library screen took 2–3 seconds (and sometimes longer) to open. This bug was unexpected as performance considerations were not explicitly detailed in Apple’s documentation or other sources.</p><p>Here are my findings on improving fetching performance.</p><h4>Use caching</h4><p>In order to apply caching using the Photos framework’s built-in mechanism, you can use the PHCachingImageManager class. This class allows you to cache multiple assets at once, and automatically caches them for fast access.</p><pre>let manager = PHCachingImageManager()<br>let assets = PHAsset.fetchAssets(with: .image, options: nil)<br>manager.startCachingImages(for: assets, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: nil)</pre><p>This will cache all assets of type “image” with a target size of 100x100 pixels and content mode of aspectFill.</p><p>The PHCachingImageManager class also provides the stopCachingImages method to stop caching images when they are no longer needed. This can help to improve the performance of your app by only caching the images that are currently being displayed.</p><p>It’s worth noting that when using the PHCachingImageManager class, you should consider your app’s specific use case and your company’s requirements. Besides, it’s important to test the app on different device models and iOS versions to make sure that it works correctly. Also, make sure to consider the performance and memory usage when working with large amounts of data.</p><p>For third-party caching libraries such as <em>SDWebImage</em> or <em>Kingfisher</em>, you can use the built-in caching functionality provided by the library. For example, <em>SDWebImage</em> provides a UIImageView extension that allows you to load an image from a URL and automatically cache it for future use.</p><pre>let imageView = UIImageView()<br>imageView.sd_setImage(with: imageURL)</pre><p><em>Kingfisher</em> also provides similar functionality, you can load an image with a URL and it will cache it for future use.</p><pre>let imageView = UIImageView()<br>imageView.kf.setImage(with: imageURL)</pre><h4>Use the correct target size</h4><p>When fetching assets from the Photo Library, you can specify the target size of the image by using the PHImageManager class. The target size is specified in pixels and represents the dimensions of the image you want to retrieve. By specifying a smaller target size, you can reduce the time it takes to fetch the image.</p><p>Here’s an example of how to fetch an image with a target size of 100x100 pixels:</p><pre>let manager = PHImageManager.default()<br>let options = PHImageRequestOptions()<br>options.deliveryMode = .fastFormat<br>options.resizeMode = .fast<br>manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: options) { (image, _) in<br>   // Use the image here<br>}</pre><blockquote>It’s important to note that specifying a smaller target size<strong> </strong>may result in a lower-resolution image. For this reason, it’s always better to consider the specific use case of your app and the needs of your users when choosing the target size. You should also consider that using a small target size will have a positive impact on the performance of your app, but using too small a target size might not be enough to display the image correctly in some scenarios.</blockquote><h4>Implement asynchronous requests</h4><p>Using <strong>asynchronous requests</strong> to fetch assets from the photo library is a good way to improve the performance and efficiency of your app. When making synchronous requests, the app will block the main thread until the request is completed, which can result in a poor user experience.</p><p>By using asynchronous requests, the app can continue running while the request is being processed in the background, resulting in a more responsive user interface.</p><p>The Photos framework provides several ways to make asynchronous requests. One of the most common ways is to use the PHImageManager class to make an asynchronous request for an image. Here’s an example:</p><pre>let manager = PHImageManager.default()<br>let options = PHImageRequestOptions()<br>options.deliveryMode = .highQualityFormat<br>options.isSynchronous = false<br>manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: options) { (image, _) in<br>  // Use the image here<br>}</pre><p>In the above code snippet, the isSynchronous property is set to false. This makes the request asynchronous. You can also specify other options, such as the delivery mode, to further optimize the request.</p><p>You can use some third-party libraries like <em>AlamoFire</em> to make async requests as well.</p><blockquote>In all cases, it’s important to note that using asynchronous requests will generally improve performance. It’s also essential to handle the responses correctly and not overload the system with too many requests at once.</blockquote><p>You can use the options provided by PHImageRequestOptions to specify how the image should be delivered and to optimize the fetching process.</p><p>The PHImageRequestOptions class provides various options such as deliveryMode, resizeMode, isNetworkAccessAllowed, version and more which you can use to fine-tune the request.</p><p>For example, you can set the deliveryMode to .fastFormat which will increase the fetching speed.</p><pre>let options = PHImageRequestOptions()<br>options.deliveryMode = .fastFormat</pre><p>This will tell the image manager to deliver the requested image as quickly as possible. When this option is set, the image manager may return a lower-quality image.</p><p>Another option you can use is resizeMode. You can set it to .fast which will cause the image manager to resize the image as quickly as possible.</p><pre>options.resizeMode = .fast</pre><p>You can also set isNetworkAccessAllowed to <em>true</em> to allow the image manager to retrieve the image from the network if it’s not available locally.</p><pre>options.isNetworkAccessAllowed = true</pre><p>PHImageRequestOptions provides a version property too. This property is used to request a specific version of an asset.</p><pre>options.version = .original</pre><blockquote>It’s important to note that when using the PHImageRequestOptions class, you should consider your app’s specific use case and your company’s requirements. Additionally, you should test the app on different device models and iOS versions to make sure that it works correctly. Make sure to also consider the performance and memory usage when working with large amounts of data.</blockquote><h4>Choose the correct image format</h4><p>Use the<strong> correct image format</strong>. The format of the image you fetch can have a significant impact on the performance of your app. When fetching images that will be displayed on the screen, you should use a format that is optimized for screen display, such as JPEG or PNG. These formats are more efficient and take up less space than other formats like TIFF or HEIF.</p><p>JPEG is a lossy format, meaning that it discards some image data to reduce file size. It’s good for images with a lot of color details such as photographs. It’s also supported by all web browsers and many image editing software.</p><p>PNG is a lossless format. It means that it preserves all the data in the image. It’s good for images with solid colors and simple shapes. It’s supported by all web browsers and many image editing software as well.</p><p>On the other hand, if you are fetching images that will be processed or manipulated, you should use a format such as HEIF or TIFF that can provide higher quality. HEIF is a new format developed by Apple. It’s designed for high-quality images and videos, and it’s also more efficient than JPEG and TIFF in terms of storage space.</p><p>TIFF is a lossless format that is good for images that need to be edited and manipulated. It’s also good for images with a lot of color details such as photographs but is not supported by all web browsers and many image editing software.</p><p>It’s important to consider the specific use case of your app and the requirements of your company when choosing the format of the image you fetch. Additionally, you should test the app on different device models and iOS versions to make sure that it works correctly and also to consider the performance and memory usage when working with large amounts of data.</p><h4>Leverage the content mode option</h4><p>When fetching an image, you can use the <strong>content mode option </strong>to specify how the image should be displayed. This can help to reduce the time it takes to fetch the image and improve the performance of your app.</p><p>The Photos framework provides several content mode options that you can use, including:</p><ul><li>PHImageContentMode.aspectFit: This content mode scales the image to fit within the specified size while preserving the aspect ratio of the original image. This can be useful for displaying small thumbnails where the entire image should be visible but doesn’t have to fill the entire space.</li><li>PHImageContentMode.aspectFill: This content mode scales the image to fill the specified size while preserving the aspect ratio of the original image. This can be useful for displaying images in a full-screen view where the entire image should be visible and fill the whole space.</li><li>PHImageContentMode.default: This content mode returns the image in its original size and aspect ratio.</li></ul><p>You can use the appropriate content mode based on your app’s specific use case and your company’s requirements. Additionally, you should test the app on different device models and iOS versions to make sure that it works correctly, and also to consider the performance and memory usage when working with large amounts of data.</p><blockquote>It’s important to note that when specifying the content mode, the framework will try to return the image in the exact size you asked for, but if the image size is not available, the framework will scale the image to match the size you asked for. Scaling images can take a long time and consume more resources on a device, so you should use the appropriate content mode for the specific use case of your app.</blockquote><h4>Conclusion</h4><p>Developing a photo library or any complex feature for an iOS app can be a challenging task, but with your skills as an iOS developer, you are well-suited to tackle it. Here are a few things to consider as you begin.</p><p>1. Understand the requirements: Before you start coding, make sure you have a clear understanding of what the photo library should do and how it should behave. Get answers to questions such as “Will users be able to upload their own photos?”, “Will the library include editing tools?”, “How will the photos be organized and displayed to users?”</p><p>2. Research existing solutions: There are many open-source libraries and frameworks available that can help you implement a photo library in your app. Look into popular options such as Photos Framework, Photos UI, and Kingfisher and see if they meet your needs.</p><p>3. Plan your architecture: Once you have a good understanding of the requirements and existing solutions, you can start planning the architecture of your library. Consider factors such as performance, scalability, and maintainability.</p><p>4. Write clean, well-commented code: As you begin coding, make sure to write clean, well-organized code that is easy to understand and maintain. Remember to add comments and documentation to help others understand your code.</p><p>5. Test and debug: As you develop the library, be sure to test it thoroughly, and debug any issues that you encounter. Make sure it works correctly on different device models and iOS versions.</p><p>6. Follow the company’s guidelines and use their tools. Also, it’s better to use the company’s RFC process to get feedback and approval before implementation.</p><p>7. Remember that coding is not the only element of your project. You should also consider the user experience, accessibility, and security.</p><p>8. Don’t hesitate to ask for help or feedback from other team members. They may have different experiences that could help you.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f3d28cbf42fe" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/balancing-innovation-and-scalability-developing-vestiaire-collectives-photo-library-feature-on-f3d28cbf42fe">Balancing Innovation and Scalability: Developing Vestiaire Collective’s Photo Library Feature on…</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How we increased our Web Google Shopping conversion by 65%]]></title>
            <link>https://medium.com/vestiaire-connected/how-we-increased-our-web-google-shopping-conversion-by-65-c1c122dcf477?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/c1c122dcf477</guid>
            <category><![CDATA[product]]></category>
            <category><![CDATA[product-management]]></category>
            <category><![CDATA[landing-page-optimization]]></category>
            <category><![CDATA[digital-marketing]]></category>
            <category><![CDATA[ecommerce]]></category>
            <dc:creator><![CDATA[Livio ERUTTI]]></dc:creator>
            <pubDate>Mon, 03 Apr 2023 13:51:06 GMT</pubDate>
            <atom:updated>2023-04-03T15:35:58.635Z</atom:updated>
            <content:encoded><![CDATA[<h4>Spoiler: do not underestimate the power of highly relevant product alternatives.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nyUWcNgqsTmDzM0t" /><figcaption>Picture by John Schnobrich — Unsplash</figcaption></figure><blockquote>“Why is our Google Shopping bounce rate much higher than our other acquisition channels?”</blockquote><blockquote>“What’s wrong with our product landing pages: is it the design or the featured info?”</blockquote><p>If you’ve ever made these comments when trying to make sense of poor acquisition campaign results, we have some good news: you’re not alone. We did too.</p><p>As part of our company’s Traffic Collective, our mission is to make buying second-hand items desirable and drive more sustainable fashion habits by growing a community of buyers and sellers. To fulfill our purpose, Google Shopping has always been one of our key acquisition channels; so much so that we strongly increased our investment in recent years, making it our first source of new visitors today.</p><p>However, we soon started to notice that we were experiencing a low conversion on this channel, limiting our ability to scale it. We decided to launch a full user journey audit to optimize our Google Shopping campaign.</p><p>In this article, we’ve gathered some key learnings on how we tackled this landing page improvement opportunity. We’ll emphasize how we identified the problem and tested our new approach to validate the performance of the new Google Shopping campaign.</p><h3>Initial user journey</h3><p>Re-clarifying the user journey was the first step in our roadmap. The overall funnel was rather classic, but we noticed distinct behaviors when visitors landed on the product pages (PDPs). We split them into three categories: Happy Case, Successful Alternative, and Unsuccessful Alternative.</p><p>You can find the full description of the flow in the image below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Qg0RWwk_FCDTEgmG" /><figcaption><em>High level customer journey from Google Shopping</em></figcaption></figure><h3>Understanding the user pain points</h3><p>When buying fashion, users look for items matching specific criteria: size, color, expected condition, etc. In that context, it is unlikely that the first item they encounter will be the right one. They want to browse alternatives.</p><p>But we already covered that pattern thanks to how we designed our product pages… Or did we?</p><p>Well, here’s what we found. When landing on a product page (PDP), visitors could:</p><ul><li>Follow the breadcrumbs to go back to a catalog page, but the usage was very low.</li><li>Start a new search/browsing session, but this also had limited adoption.</li><li>Browse recommendations, but they were below the fold and not always available.</li><li>Go back to Google Shopping, which was the <strong>main journey.</strong></li></ul><p>And there it was: the main source of our problems. Because we were not answering our users’ need to easily find product alternatives on Vestiaire Collective, our Google Shopping campaigns resulted in high bounce and low conversion rates.</p><p>From then on, we focused on one single goal: help our users find product alternatives by browsing our platform.</p><h3>What we learned from the data</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*egmDRR6ekrTOuh3D" /><figcaption><em>Vestiaire Collective Listing page</em></figcaption></figure><p>We further analyzed the behavior of our clients and discovered that users landing on the listing pages had a much lower bounce rate and a higher CVR than those landing on the product pages. So we were able to formulate the following hypothesis: if users can more easily find product alternatives on our catalog pages, they’ll spend more time discovering our platform and purchasing items.</p><h3>Our solution design</h3><p>We opted for a first new design showing a listing of items from the same brand and category as the clicked item in our campaign landing pages. We knew this improvement could bring value as these criteria are the most used in users’ searches.</p><p>The technical solution had a lot of flexibility as any catalog path could be associated with any product allowing us to test multiple combinations (adding/removing criteria).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/664/0*Iz9adlhvzQ5SF1PX" /><figcaption>Vestiaire Collective Shopping landing page</figcaption></figure><h3>Testing approach and results</h3><p>When we started the tests, our website did not have proper A/B testing capabilities. So we had to find a workaround leveraging the ones of our product feed partner.</p><p>You can have a look at the full flow below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5ay4shNAnS71R8sJ" /><figcaption><em>Testing approach illustration</em></figcaption></figure><p>Verdict? The results were excellent! We observed a substantial uplift in our Shopping traffic KPIs:</p><ul><li><strong>-20%</strong> Bounce rate</li><li><strong>+24%</strong> Product page views per session</li><li><strong>+65%</strong> Conversion rate of New visitors to New buyers within seven days</li></ul><h3>Next up? Iterate and learn</h3><p>We believed there were still improvements we could make to optimize the conversion rate. Because of limited A/B testing capabilities at the time, we had to make trade-offs to be able to move on while keeping a rational approach.</p><h4>1. Adding a model criteria</h4><p>Bags are one of the top-sold categories on our platform. Also, half of the bags sold on Vestiaire Collective have an identified model, which is a key purchasing criterion for the users. We hypothesized that filtering on the brand, category, and model could improve the relevancy of contents when that information was available.</p><p>Consequently, we launched another Productsup A/B test, measuring the ads’ ROI metric in Italy and the US.</p><p><strong>Conclusion</strong> → Results were significantly better. We decided to roll out.</p><h4>2. Showing the Welcome Offer on the Google Shopping landing page</h4><p>New users can benefit from a special offer for their first order on the platform, which is a strong incentive to browse and convert. This discount was already being highlighted on the classic product page but not on our Shopping landing page. We launched a geo test and saw a positive trend in geographies where the Welcome Offer was displayed. This confirmed data from previous A/B tests indicating that showing the Welcome Offer positively impacts new users’ conversion in general.</p><p><strong>Conclusion</strong>: We also chose to roll out this change.</p><h3>Key takeaways</h3><p>Here are the top three takeaways to remember from this Google Shopping campaigns revamp journey.</p><ol><li>Mobile-first doesn’t mean mobile only: Although Vestiaire Collective has a mobile-first strategy, our web platform represents 85% of our new traffic. Hence, optimizing it to help new users discover our products and keep growing our business is crucial.</li><li>Do not underestimate the complexity of analyzing A/B test results: This was a challenge for this project, as we had to analyze the browsing behaviors on our website while the split was done in our tool to operate feeds.</li><li>Showing the Welcome Offer from the beginning of the journey is a key incentive for users to purchase items.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vfqO_pq3d9W5R2h1" /><figcaption>Picture by Hannah Morgan — Unsplash</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c1c122dcf477" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/how-we-increased-our-web-google-shopping-conversion-by-65-c1c122dcf477">How we increased our Web Google Shopping conversion by 65%</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How we scaled a Data microservice on Kubernetes]]></title>
            <link>https://medium.com/vestiaire-connected/how-we-scaled-a-data-microservice-on-kubernetes-944e0afbe6c0?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/944e0afbe6c0</guid>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Tanakorn Kriengkomol]]></dc:creator>
            <pubDate>Thu, 30 Mar 2023 07:58:58 GMT</pubDate>
            <atom:updated>2023-03-31T09:07:54.518Z</atom:updated>
            <content:encoded><![CDATA[<h4>The story of how our data team performed load testing to validate the scalability of one of their key microservices.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vgcEq1eWHi0ArJgq" /><figcaption>Kubernetes services | Growtika via <a href="https://unsplash.com/fr/photos/KU9ABpm7eV8">Unsplash</a></figcaption></figure><h3>What is load testing?</h3><p>Load testing is a performance test that focuses on measuring a software’s response under different real-world load conditions.</p><p>This phase is highly important in the lifecycle of a microservice. It is the main piece of the puzzle that can ensure that a software will handle the load as expected in reality.</p><p>At Vestiaire Collective, <a href="https://predator.dev/">Predator</a> is our official tool for all load tests, and it’s maintained by our Vestiaire Collective platform team. It’s a powerful, flexible tool that we leverage to perform unlimited tests at low cost.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TBjdk-2euCK1gqnC" /><figcaption>Predator UI</figcaption></figure><p>In this article, we’re going to walk through each load testing step of our CRIME service. CRIME is an in-house software dedicated to flagging counterfeit products. Recently, we developed a new Machine Learning model and implemented it into CRIME.</p><p>The goal of this post is to share the strategy and learnings that came along with the load testing process of CRIME, so that you can better understand how we make sure that all of the microservices we ship to Production are scalable.</p><p>At the end of the performance improvement phase, we wanted CRIME to:</p><blockquote>1. Be able to handle 50 RPS.</blockquote><blockquote>2. Have a p95 response time &lt; 1 second.</blockquote><h3>A little context</h3><p>In the next sections, we will mention elements that play a role in the CRIME microservice architecture, namely Snowflake, DatAPI, Pricing service, DataDog and Grafana. It’s important for you to get a rough idea of why they’re important to better understand the load testing process of CRIME.</p><p>Here’s a brief glossary of those elements. Don’t hesitate to come back to these definitions later in your reading!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/844/0*4mBNNAojFXyKL12R.png" /><figcaption>Technical glossary</figcaption></figure><p>Since a picture is worth a thousand words, here’s a diagram of the CRIME service architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2vpUWhSZtsWhtW_P" /><figcaption>CRIME service architecture</figcaption></figure><h3>Let’s put CRIME to the test</h3><p>The test was done in the production environment by gradually increasing the load on CRIME. This way, we could ensure the smallest possible impact on other microservices relying on it.</p><p>You can find the results in the below table.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1002/0*dhZS4qWdxZ2GUYNw.png" /><figcaption>First load testing results for CRIME</figcaption></figure><p>*At 25 RPS (Requests Per Second), the load caused high latency on the CRIME service and affected other production calls. When checked against other dependent services, here’s what we saw.</p><h4>Database query latency</h4><p>DatAPI uses PostgreSQL as a back-end database.</p><p>The table that is used for serving features of CRIME doesn’t have an index on the key column.</p><h4>Spike in CPU usage</h4><p>CPU utilization of CRIME service went up by a large margin compared to the usual load.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*foY3oKs5oCm6K_Pk" /><figcaption>Metrics from Grafana</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kHyBK50jUDEw_uqw" /><figcaption>Metrics from Datadog</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kvDnrjp4DKz5xw8i" /><figcaption>Other metrics of the service during the load test</figcaption></figure><p>The spikes in figures 4 and 5 represent the increase in CPU usage for the following load-testing scenarios: 10 RPS, 20 RPS, and 25 RPS (canceled early) respectively. Both Grafana and Datadog pictures show roughly the same period.</p><h4>What we concluded</h4><p>CRIME was able to serve at most around 20 RPS for a short time (testing tasks lasted 5 minutes each) and was very sensitive to DatAPI performance.</p><h4>Possible improvements</h4><p>Thanks to the various tests, we were able to identify three different areas of improvement that could boost CRIME’s performance.</p><ol><li>Add an index on the table in PostgreSQL.</li></ol><p>2. Change the configuration of our pods’ CPU and Memory.</p><p>3. Increase the number of serving pods for CRIME and DatAPI.</p><h3>Optimizing CRIME in preproduction environment</h3><p>Every time we get to the optimization phase, our idea is to get a general feeling of what change will be most impactful. That’s why we decided to optimize CRIME based on two axes: CPU and max replicas.</p><p>The tests were all done using the below settings in Predator.</p><blockquote>Starting RPS: 10 RPS</blockquote><blockquote>Ramp to: 100 RPS</blockquote><blockquote>Duration: 10 min</blockquote><h4>Baseline: Initial configuration before optimization</h4><p>First, we set the initial CRIME preproduction pod configuration to match the production. It was useful to start optimizing in a baseline environment as close to the production environment as possible. We later tweaked this configuration to improve CRIME’s performance.</p><p>Here is the initial configuration of the CRIME pod.</p><blockquote>CPU: 200m, 400m</blockquote><blockquote>Memory: 500Mi, 1Gi</blockquote><blockquote>Min Replicas: 2</blockquote><blockquote>Max Replicas: 3</blockquote><blockquote>Target CPU Utilization: 70%</blockquote><p><strong>Results</strong></p><p>RPS maxed out at around 27–30 RPS and caused a bottleneck that made the rest of the requests stagnate. CPU was also maxed out and could not serve more requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*y6ypXVGUAL31Hb_T" /><figcaption>Initial configuration</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yvCgAw64lz2g_GfH" /><figcaption>CPU utilization for each pod — Initial configuration</figcaption></figure><h4>Optimization #1: Increase max replica pods</h4><p>We increased the maximum number of replicas from 3 to 10.</p><blockquote>CPU: 200m, 400m</blockquote><blockquote>Memory: 500Mi, 1Gi</blockquote><blockquote>Min Replicas: 2</blockquote><blockquote>Max Replicas: 10</blockquote><blockquote>Target CPU Utilization: 70%</blockquote><p><strong>Results</strong></p><p>Increasing max replicas did help to a certain extent but the response time was still too high.</p><p>Also, it did not scale up to more than 10 replicas and maxed out at around 6–7 instances.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7lUi4BVXkj3FkpQ4" /><figcaption>Only increase max replicas</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bASDjz4cN3h3Fs3W" /><figcaption>CPU utilization by each pod — Increase replica</figcaption></figure><h4>Optimization #2: Increase CPU size</h4><p>We finally increased the CPU from 200m, 400m to 700m, 1.2.</p><blockquote>CPU: 700m, 1.2</blockquote><blockquote>Memory: 500Mi, 1Gi</blockquote><blockquote>Min Replicas: 2</blockquote><blockquote>Max Replicas: 3</blockquote><blockquote>Target CPU Utilization: 70%</blockquote><p><strong>Results</strong></p><p>Increasing CPU size seemed to help much more than purely increasing the maximum number of replicas. With this configuration, we concluded that we should be able to serve requests at a maximum of 50 RPS, which was our target!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oEjXEHm3SdW6xP7a" /><figcaption>Only increase CPU size</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WlphfY-Ax6Y6kUYO" /><figcaption>CPU Utilization by each pod — Increase CPU</figcaption></figure><p><strong>Note</strong></p><p>Most of the time within requests was spent waiting for external calls to return their outputs. The performance bottleneck was not caused by the model inference as it took less than 50 ms to complete for most of the calls.</p><h3>Results of optimization in production</h3><p>After moving from initial findings to the testing of multiple configurations in CRIME service, we successfully reached the end of our optimization process.</p><p>CRIME could now handle loads of 40 RPS, considering that there were no spike loads on external dependencies services i.e. DatAPI and pricing service.</p><p>The below charts are load testing results obtained from the production environment.</p><p>Most of the time request latency was under 1 second. However, we could still observe high latency spikes due to CRIME spawning new instances, as with the current implementation. This happens because CRIME initialize and set up their models for inference when starting up their containers (cf. cold start behavior).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ANN629w94ka3QsNo" /><figcaption>Load test latency — final configuration</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_CPyuDiAzgACUUAm" /><figcaption>Load test RPS — final configuration</figcaption></figure><h4>Final configuration</h4><blockquote>CPU: 500m, 1000m</blockquote><blockquote>Memory: 500Mi, 1Gi</blockquote><blockquote>Min Replicas: 2</blockquote><blockquote>Max Replicas: 10</blockquote><blockquote>Target CPU Utilization: 60%</blockquote><h3>Conclusion</h3><p>Although we did not reach the target RPS of 50 RPS, the number we achieved after optimization is good for our planned use case. On the response time side, it should be more than good enough, as most requests were responded within 1 second.</p><p>For the CRIME service, most of the bottlenecks were coming from insufficient CPU resources. Increasing the CPU size for each pod really helped scale up the load the service could handle. But as the service still has dependency on DatAPI, we will need to look into how to improve it as a next step to ensure that all our data team services are working well together.</p><p>This optimization was only possible thanks to the good tooling available to us. Predator as a load-testing tool gives us a very easy time when iterating on a change and seeing the impact immediately after. In addition, both Datadog and Grafana — for service monitoring — give us a detailed view of the service performance and give valuable insights into where the bottlenecks are.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*puyziOvatc7-0hkz" /><figcaption>Picture by Fab Lentz via Unsplash</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=944e0afbe6c0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/how-we-scaled-a-data-microservice-on-kubernetes-944e0afbe6c0">How we scaled a Data microservice on Kubernetes</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Designing Vestiaire Collective’s private messaging feature]]></title>
            <link>https://medium.com/vestiaire-connected/designing-vestiaire-collectives-private-messaging-feature-1769c015d600?source=rss----c1311d187d2b---4</link>
            <guid isPermaLink="false">https://medium.com/p/1769c015d600</guid>
            <category><![CDATA[product-management]]></category>
            <category><![CDATA[fashion]]></category>
            <category><![CDATA[product]]></category>
            <category><![CDATA[product-development]]></category>
            <category><![CDATA[apps]]></category>
            <dc:creator><![CDATA[Charly Leporc]]></dc:creator>
            <pubDate>Fri, 17 Mar 2023 15:57:41 GMT</pubDate>
            <atom:updated>2023-03-17T15:57:41.779Z</atom:updated>
            <content:encoded><![CDATA[<h4>Our journey to enable our community with a safe user-to-user chat.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*pjFkqeUxyjKBdZJuhjAg2w.png" /></figure><h3>What is Buyer-Seller Chat?</h3><p>Are you a fellow user of the Vestiaire Collective app? In that case, you’re probably used to checking your messages in your notification center. Internally, we call this space buyer-seller chat.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/358/0*0WXyDTEPjNquFwhV.png" /><figcaption>New message in the Buyer-Seller chat</figcaption></figure><p>Buyer-seller chat is Vestiaire Collective’s private messaging feature on desktop and mobile. It is the go-to place for buyers and sellers to communicate about products for sale and orders in progress. Since its early days, it has become a key feature of the experience to boost transparency and conversion.</p><p>Our teams completed many design and development iterations to make the chat as safe and user-friendly as it is today. This article will shed some light on how we came up with the solution that is now live on Vestiaire Collective.</p><p>We hope you’ll take away some useful lessons from our failures and successes for your own projects.</p><p>So without further ado, let’s get started!</p><h3>Defining the needs</h3><p>The private messaging feature is one of the most critical features that Vestiaire Collective launched in 2020. Our members had been in need of this kind of feature for a long time.</p><h4>Restricted interaction capacities</h4><p>Before buyer-seller chat became available, our members had to use the comment sections on product pages to ask for more product details. The flow was a bit cumbersome. Plus, real-time communication was impossible for comments moderated by our teams.</p><p>The other way our members could connect was via our negotiation rooms, called <em>Make Me An Offer</em> (MMAO), which allow our buyers and sellers to propose discounts on their items. Here again, members were very limited in their interactions because they could only exchange prices.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/384/0*OUbdLzhBljJn8-7_.png" /><figcaption>MMAO negotiation room</figcaption></figure><h4>Business challenges: staying competitive while maintaining our safety standards</h4><p>Most Marketplace platforms offer a private messaging feature as their primary interaction tool. Consequently this became an expectation for all users, and not having it put us behind in terms of feature parity.</p><p>Another concern was the risk of marketplace circumvention. The role of a marketplace platform is to allow safe transactions and to protect both sides of the platform. Sellers need to be paid and buyers need to receive the item they paid for.</p><p>Multiple questions remained that we could not answer unless we launched the new chat to market:</p><ul><li>Since Vestiaire Collective is a global platform, could it limit the risk of people taking their trades outside the marketplace?</li><li>Vestiaire Collective’s average basket size is significantly larger than the competition. Would selling out of the platform be consequently riskier for both our buyers and sellers?</li><li>We provide control and authentication of our products. Shouldn’t that be a sufficient reason to keep sales on the platform?</li></ul><h4>Product challenges: UX coherence and efficient development</h4><p>On top of those business questions, we also ran into product development challenges in developing such a feature without going for a complete revamp of the app:</p><ul><li>How could we make our <em>Make Me An Offer</em> feature coexist with a private messaging feature?</li><li>What control mechanism should we implement to monitor potential marketplace circumvention?</li><li>Should we build the entire feature in-house or use a third party?</li></ul><h3>The design phase</h3><p>One of my favorite quotes that I remember from my design class is, “a good design answers the brief”. For this project, the brief was “to allow buyers to start a chat and enable sellers to answer a chat without touching a single piece of the current <em>Make Me an Offer</em> feature code.”</p><p>The requirements immediately put challenging constraints on the design team, which was already considering blending the two experiences. Blending the experience of chatting and making a price offer would make perfect sense if we had to build the feature from scratch, but that would have led us to touch so many pieces of our architecture that the project would have taken months.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/376/0*US_DmZYPhmtiCrm0.png" /><figcaption>Prototype blending the chat with the negotiation feature</figcaption></figure><p>​Funnily enough, what we thought was a constraint actually ended up being an important feature. Dissociating the <em>Make Me An Offer</em> feature from the private messaging feature would allow us to enable one or the other for different users and be more nimble in deciding certain rules.</p><h3>The technical solution</h3><p>While our design team was working on the mockups, our engineering team and our product manager looked at third-party solutions. As exciting as it sounded for the engineering team to develop such a feature, we also had some expectations regarding time to market and were convinced that finding a backbone would help us gain time.</p><p>We decided to shortlist two vendors and spent two sprints building a basic proof of concept with the providers to decide which one would be the right solution to create a feature that we knew was likely to stay for a while. We definitely did not want to choose the wrong architecture, so we gave the time to our Front end, Back end, and Mobile engineers to play with the solution and decide which option they would feel most comfortable building on.</p><p>We based our comparisons on the following characteristics:</p><ol><li>Scalability</li><li>The coding language used</li><li>The available features</li><li>The reactivity of the third-party tech team</li><li>The price of the solution</li></ol><p>A few months after launching the feature, we were very happy about our decision and, more importantly, about the process we put in place. When working on such a project, it is essential to obtain approval from all stakeholders, so everyone feels engaged in delivering the best product possible.</p><p>This was also a good example of how important it is to deepdive on all the technical aspects of a third-party partner. A private messaging feature can potentially deal with thousand of messages simultaneously. The coding language of the service is important in order to make as much of a saving as possible on IT infrastructure. Any increase of load on our partner infrastructure would inevitably result in additional costs on our side. We would be tied to a partner that couldn’t scale without impacting our ROI. This could be the topic of another post: how infrastructure cost is often forgotten when designing a feature or engaging in commercial activities.</p><h3>Focusing on the most impactful KPIs from Day 1</h3><p>As with every new product initiative, setting the proper KPIs is the first step to success. Within Vestiaire Collective, we tend to have different stakeholders for initiatives impacting our daily active users or conversion. Since such a feature could impact both engagement and conversion, it was even more vital for us to define the key metrics we would follow up on to iterate on our MVP. Do we want to create a feature that brings buyers and sellers back to the platform on a daily basis? Or do we prefer to optimise it to help sellers sell faster?</p><p>We settled on two main success metrics related to conversion: the adoption and success rate, which we defined as follows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lYDiKAEf6jTR1Uyg.png" /></figure><p>We also built a monitoring dashboard to examine proxy metrics that helped us monitor marketplace circumventions. One good example is the number of products taken off the platform after a chat was started.</p><p>We were ready to launch our product iteratively and be aware in real time of how it was impacting our business positively while making sure we were managing risk.</p><h3>Managing the risks</h3><p>As described above, launching this product was planned to bring value to our users while simultaneously introducing the risk of more transactions happening outside the platform.</p><p>We ran a few brainstorming sessions with our engineers, designers, product managers, and business stakeholders to consider the different options to manage this hazard.</p><p>One might think that bringing added value to users would encourage them stick to the platform and prevent sales from happening outside of it. However, we could not preclude this from happening to some users. Not only are circumventions a source of revenue loss for the company, they are also a threat to our members, who trust us as a safe and controlled marketplace.</p><p>Making a transaction secure and preventing counterfeit products from being sold on the platform is the core of our business. Therefore, we started to think about a few features that would at least create a safety net to prevent misbehaviour.</p><h4>Dictionary search</h4><p>The first one we put in place was a dictionary-based flag mechanism. Here is how it worked in a nutshell:</p><ul><li>A user wrote a message</li><li>Our algorithm checked the message against a dictionary of inappropriate words</li><li>If we detected unacceptable words, our solution would remove the input and replace it with a statement explaining the reasons.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/550/0*vZaEkXQrjgH628kl.png" /><figcaption>Automatic message after a message deletion</figcaption></figure><p>The first iteration relied on quite a strict dictionary. It evolved manually and we later added a layer of automation to make it smarter at catching the most inappropriate words in every language while allowing all appropriate messages to go through.</p><p>This dictionary became the backbone of more features that we then started to add.</p><h4>Automated user bans</h4><p>We began to ban users who broke our guidelines multiple times and automatically sent emails to educate them about the good practices around using our buyer-seller chat.</p><h4>Reporting harmful messages</h4><p>We also included a report option. We were very positively surprised about the number of members from our community who were proactively denouncing misbehaviour. They significantly contributed to helping us strengthen our engine and making our private messaging feature safer and safer.</p><p>As stated before, keeping the chat separate from our <em>Make Me An Offer</em> feature enabled us to turn it off for given users while still allowing them to make and receive offers. Merging the two features would have made that more complicated. This was an excellent example of how a constraint can actually turn into a nice feature.</p><h3>Conclusion</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/860/0*niSaFhfxaTK2DqRZ.png" /><figcaption>Risk vs Reward | Unsplash</figcaption></figure><p>We opted for a canary deployment strategy, releasing the update incrementally to ever-growing subsets of users starting at the end of summer 2020. Two years after the launch, up to 25% of our transactions are already happening with a private message. On the other hand, the number of conversations that we judged inappropriate was below 3% at the time of the launch. As a result, buyer-seller chat proved to bring more benefits than drawbacks, and our sellers praised the feature as it made them more likely to sell.</p><p>Ultimately, this confirms that solving a user’s problem and making their life easier should drive every product development process.</p><blockquote><em>What do you think we should add to this feature?</em></blockquote><p>If you have any questions about our processes and how we made it happen, feel free to drop us a comment.</p><p><em>We are always on the lookout for product folks to share the ride. Visit us </em><a href="https://fr.linkedin.com/company/vestiaireco"><em>here</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1769c015d600" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vestiaire-connected/designing-vestiaire-collectives-private-messaging-feature-1769c015d600">Designing Vestiaire Collective’s private messaging feature</a> was originally published in <a href="https://medium.com/vestiaire-connected">Vestiaire Connected</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>