{"id":23093028,"url":"https://github.com/crispengari/apr-dl","last_synced_at":"2026-04-13T19:31:58.303Z","repository":{"id":141093813,"uuid":"490663721","full_name":"CrispenGari/APR-dl","owner":"CrispenGari","description":"⭐⭐⭐⭐⭐ Automatic Product Recommender (APR) is an Artificial Intelligence GrpahQL API for recommending products based on customer reviews.  👍👎 ","archived":false,"fork":false,"pushed_at":"2024-06-28T11:37:28.000Z","size":57227,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-20T00:30:06.059Z","etag":null,"topics":["api","artificial-intelligence","deep-learning","graphql","java","machine-learning","natural-language-processing","python","rest","rest-api","ruby","tensorflow","text-classification","typescript"],"latest_commit_sha":null,"homepage":"","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CrispenGari.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-10T11:17:58.000Z","updated_at":"2024-06-28T11:37:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"6afe5631-c10f-4db8-9e7b-dfdf5290cf45","html_url":"https://github.com/CrispenGari/APR-dl","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/CrispenGari/APR-dl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CrispenGari%2FAPR-dl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CrispenGari%2FAPR-dl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CrispenGari%2FAPR-dl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CrispenGari%2FAPR-dl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CrispenGari","download_url":"https://codeload.github.com/CrispenGari/APR-dl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CrispenGari%2FAPR-dl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31768636,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T15:25:13.801Z","status":"ssl_error","status_checked_at":"2026-04-13T15:25:09.162Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","artificial-intelligence","deep-learning","graphql","java","machine-learning","natural-language-processing","python","rest","rest-api","ruby","tensorflow","text-classification","typescript"],"created_at":"2024-12-16T21:46:20.558Z","updated_at":"2026-04-13T19:31:58.285Z","avatar_url":"https://github.com/CrispenGari.png","language":"Jupyter Notebook","funding_links":[],"categories":[],"sub_categories":[],"readme":"### Automatic Product Recommender\n\n⭐⭐⭐⭐⭐ Automatic Product Recommender **(APR)** is an Artificial Intelligence API for recommending products based on customer reviews. 👍👎\n\n\u003cimg src=\"./images/cover.png\" width=\"100%\" alt=\"cover\"/\u003e\n\n### Table of Contents\n\n- [Automatic Product Recommender](#automatic-product-recommender)\n- [Table of Contents](#table-of-contents)\n- [Abstract](#abstract)\n- [Folder structure](#folder-structure)\n- [Data.](#data)\n- [Data Analysis](#data-analysis)\n- [Model Metrics/Results](#model-metricsresults)\n- [Model Inference](#model-inference)\n- [GraphQL](#graphql)\n- [Example Prediction](#example-prediction)\n- [Model](#model)\n- [Top Techs used](#top-techs-used)\n- [Notebooks](#notebooks)\n\n### Abstract\n\nAutomatic Product Recommender **(APR)** is a very useful topic in modern technologies. In this repository I present an **APR** graphql API served locally for recommending products based on other customer's reviews. APR is very useful in modern technology because it helps product consumers to chose weather they should buy the product or not based on previous experience customer reviews.\n\n### Folder structure\n\nThe folder structure of the server looks as follows:\n\n```\nC:.\n├───app\n│   └───__pycache__\n├───model\n│   └───static\n├───resolvers\n│   ├───mutations\n│   └───queries\n│       └───__pycache__\n└───utils\n    └───__pycache__\n```\n\n### Data.\n\nThe dataset that was used in this project was obtained from public datasets from [Kaggle.com](https://www.kaggle.com/) and the dataset name was [womens-ecommerce-clothing-reviews](https://www.kaggle.com/datasets/nicapotato/womens-ecommerce-clothing-reviews). The dataset contains a csv file that has a lot of columns. We cleaned the data in [this notebook](/notebooks//00_E_Commerce_Clothing_Reviews_Data_Prep.ipynb), by cleaning the text and do features and labels extraction.\n\nInitially the dataset was having `23, 486` paired rows of features to their respective labels but after removing NA values in the dataset we had the following rows of paired features with their respective labels.\n\n| #                           | # of Rows |\n| --------------------------- | --------- |\n| Before removing `NA` values | `23, 486` |\n| After removing `NA` values  | `22, 641` |\n\nInitially we had `10` column names and after the dataset has been cleaned we were left with `4` columns which contains 2 feature columns and 2 labels columns. These columns are:\n\n| Column Name   | Description                                                                                     | Category  |\n| ------------- | ----------------------------------------------------------------------------------------------- | --------- |\n| `text`        | The review text by a customer.                                                                  | `feature` |\n| `upvotes`     | Number of customers who positively vote for the review as positive.                             | `feature` |\n| `rating`      | Product score granted by the customer from 1 Worst, to 5 Best.                                  | `label`   |\n| `recommended` | stating where the customer recommends the product where 1 is recommended, 0 is not recommended. | `label`   |\n\nDuring the process of data cleaning the review text was cleaned by removing:\n\n1. punctuations and extra white spaces\n2. links and html tags\n3. numbers\n4. word's were de-contracted, meaning words like `I'm` was converted to `I am`.\n\n### Data Analysis\n\nIn this section we are going to do graphical representation of our data. We are going to start by having a look at the distribution of features and labels. The data analysis was done in the second notebook [`01_Automatic_Product_Recommender_(APR).ipynb`](\u003c/notebooks/01_Automatic_Product_Recommender_(APR).ipynb\u003e) before a deep learning model was created.\n\n1. `upvotes`\n\nAfter the data cleaning. Here is the how the upvotes were distributed with the maximum up votes on the review to be `122` and the minimum number of votes to be `0`.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/upvotes_distribution_0.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe dataset was trimmed by balancing the dataset based on the `recommended` label. And the final distribution of the upvotes was as follows having the maximum number of votes on a review to be `177` and the minimum remain `0` meaning in the dataset the review that has minimum votes has `0`.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/upvotes_distribution-1.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\n2. `rating`\n\nBefore the data was balanced with the review label. Here is how the ratings on the reviews were distributed. Most of the products that was reviewed was `5` star rated having a fraction of above `50%`. The product that wasn't having a good ratting received have a fraction of `3.6%`\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/rating_distribution_0.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nAfter the dataset was balanced based on the `recommended` label this is how the ratings were distributed in the entire dataset.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/rating_distribution_1.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\n3. `recommended`\n\nWe had more products that were recommended before we balanced the dataset based on this column and this is how the recommended label was distributed in the entire dataset.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/recomment_distribution_0.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe balanced dataset had `4, 101` examples or reviews that were recommending the product and `4, 101` reviews that were not recommending the product.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/recomment_distribution_1.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\n4. `text`\n\nWe wanted to check what words were appearing the most in the dataset. Before we balanced our dataset the words that were appearing the most were:\n\n- dress\n- love\n- top\n- fit\n- look\n\nJust to mention a few and this distribution was visualized using a `word cloud`\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/wc_0.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nAfter the dataset was balanced based on the `recommend` column the word cloud visualization was as follows:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/wc_1.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\nThe most appearing words were:\n\n- dress\n- top\n- look\n- fit\n- love\n\nWe concluded that the reviews were based on the women clothes, because of the words that were detected in the dataset from the text review columns.\n\nAfter the dataset was balanced, The dataset was spited into `3` sets preparing for the creation of a deep learning model. These sets were:\n\n1. `training` - The set that will be used to train the model.\n2. `validation` - The set that will be used to validate the model during training and updating of model metrics during model training.\n3. `testing` - The set that we will use to evaluate the model after we have trained it.\n\nThe total number of examples that we obtained after we split our dataset was as follows:\n\n| SET              | EXAMPLES |\n| ---------------- | -------- |\n| **`training`**   | `5, 741` |\n| **`testing`**    | `1, 230` |\n| **`validation`** | `1, 231` |\n\n### Model Metrics/Results\n\nWe created a model using `Keras Sub Classing` API for flexibility our model takes in `2` features and predict `2` labels, weather the product can be `recommended` or not which is a `binary classification` and it also classify or predict the `rating` that a product can get from a user based on the the review text and it's `upvote` count which was a multi-class classification task. The model was observed to have the following parameters\n\n| #                              | PARAMETERS     |\n| ------------------------------ | -------------- |\n| **`Total Parameters`**         | `4, 571, 130 ` |\n| **`Trainable Parameters`**     | `4, 571, 130`  |\n| **`Non-trainable Parameters`** | `0`            |\n\n\u003e The model had about `4.5` total trainable parameters.\n\nAfter the model was trained the training and validation metrics were stored in a history object so that they can be visualized.\n\nThe model overall `losses` for the `train` and `validation` during model training on each train epoch were as follows:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/loss.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe model individual `losses` for the `train` set that were observed during model training for the `recommended` and `ratting` labels on each train epoch were as follows:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/train_recommended_ratting_loss.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe model individual `losses` for the `validation` set that were observed during model training for the `recommended` and `ratting` labels on each train epoch were as follows:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/val_losses.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe train and validation accuracies were observed for the recommendation label per each and every epoch and were as follows:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/recommended_accuracies.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\n\u003e The accuracies kept on increasing till they stopped increasing that's where we stopped training the model using a technique called `Early Stopping`. We were not interested in the `accuracies` of the `rating` label as it is not a good measure in this case because the labels were not balanced.\n\nThe model was the evaluated based on the dataset that it have never seen during training and the following metrics were observed.\n\n| METRIC                     | VALUE  |\n| -------------------------- | ------ |\n| **`total loss`**           | `1.56` |\n| **`rating loss`**          | `1.17` |\n| **`recommended loss`**     | `0.39` |\n| **`rating accuracy`**      | `0.51` |\n| **`recommended accuracy`** | `0.86` |\n\nThe loss of the ratting label was higher than the one for the recommended label. This might be because the rating label was not balanced and had few examples on other labels. The overall loss of the `APR` model was quite reasonable and it can be improved in future works with a good dataset. The `rating accuracy` was just above average by `1%` this is because we had mis balanced labels on the `rating` labels, so we can't use this as a good measure on how good is our model performing. The model was having a reasonable accuracy of `~87%` which can also be improved with future works.\n\nWe went ahead to create make predictions based on the test dataset and the following confusion matrix were plotted.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/recommended_cm.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe above is the confusion matrix for the recommended label. And the model was really doing well.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./images/rating_cm.png\" alt=\"apr\" width=\"400\"\u003e\n\u003c/p\u003e\n\nThe model poorly performs on the rating labels due to the reasons that we mentioned which we can focus on on the future works. The classification report were as follows:\n\n|                    | precision | recall | f1-score | support |\n| ------------------ | --------- | ------ | -------- | ------- |\n| **`0`**            | `0.88`    | `0.84` | `0.86`   | `619`   |\n| **`1`**            | `0.85`    | `0.89` | `0.87`   | `611`   |\n|                    |           |        |          | `-`     |\n| **`accuracy`**     |           |        | `0.87`   | `1 230` |\n| **`macro avg`**    | `0.86`    | `0.86` | `0.86`   | `1 230` |\n| **`weighted avg`** | `0.87`    | `0.86` | `0.86`   | `1 230` |\n\nThe above is the classification report of the `recommended` label which was obtained after evaluating the model using the test data.\n\n|                    | precision | recall | f1-score | support |\n| ------------------ | --------- | ------ | -------- | ------- |\n| **`0`**            | `0.00`    | `0.00` | `0.00`   | `177`   |\n| **`1`**            | `0.38`    | `0.37` | `0.38`   | `236`   |\n| **`2`**            | `0.37`    | `0.51` | `0.43`   | `282`   |\n| **`3`**            | `0.30`    | `0.07` | `0.11`   | `191`   |\n| **`4`**            | `0.60`    | `0.94` | `0.78`   | `404`   |\n|                    |           |        |          | `-`     |\n| **`accuracy`**     |           |        | `0.51`   | `1 230` |\n| **`macro avg`**    | `0.35`    | `0.38` | `0.34`   | `1 230` |\n| **`weighted avg`** | `0.43`    | `0.51` | `0.45`   | `1 230` |\n\n### Model Inference\n\nIn this section we are going to show how you can use the `APR` server to make predictions locally.\n\nFirst you are required to have `python` installed on your computer to be more specific `python` version 3 and `git`\n\nThen you need to clone this repository by running the following command:\n\n```shell\ngit clone https://github.com/CrispenGari/APR-dl.git\n```\n\nAnd then you navigate to the server folder of this repository by running the following command:\n\n```shell\ncd APR-dl/server\n```\n\nNext you are going to create a virtual environment `venv` by running the following command:\n\n```shell\nvirtualenv venv\n```\n\nThen you need to activate the virtual environment by running the following command:\n\n```shell\n.\\venv\\Scripts\\activate.bat\n```\n\nAfter activating the virtual environment you need to install the required packages by running the following command:\n\n```shell\npip install -r requirements.txt\n```\n\nThen you are ready to start the server. To start the server you are going to run the following command:\n\n```shell\ncd api \u0026\u0026 python app.py\n```\n\nThe above command will start the local server at default port of `3001` you can be able to make request to the server.\n\n### GraphQL\n\nWith only this you will be able to test the `API` using the graphql playground that is served at the following endpoint:\n\n```py\nhttp://127.0.0.1:3001/graphql\n\n# Or\nhttp://localhost:3001/graphql\n```\n\nOpen the above url in the browser and start making predictions.\n\n### Example Prediction\n\nIn this section I will show how to make predictions to the apr-graphql-server. Note that this is an advanced way of doing it (using fragments) but if you are familiar with graphql you know the easy way of doing it. Send the following graphql request:\n\n```\nfragment ErrorFragment on Error{\n  field\n  message\n}\nfragment RatingFragment on Rating{\n  rating\n  probability\n  stars\n}\nfragment RecommendFragment on Recommend{\n  label\n  class_\n  probability\n  emoji\n}\nfragment PredictionsFregmant on Predictions{\n\trecommend{\n    ...RecommendFragment\n  }\n  rating{\n    ...RatingFragment\n  }\n}\nquery APRPredictor($input: APRInput!){\n  predictor(input: $input){\n    error{\n      ...ErrorFragment\n    }\n    ok\n    predictions{\n      ...PredictionsFregmant\n    }\n  }\n}\n```\n\nWith the following graphql variables:\n\n```json\n{\n  \"input\": {\n    \"text_review\": \"Just piping in here ordered my usual size of small petite the slip that came with the dress is about a size negative zero it could hardly squeeze over my body and the dress itself is a bright pale aqua and it is a shift and because of the smocking it very strangely i think it very cheap and is ill fitting i would say if you are a person on which shift look awesome you might like this but mind the size of the slip in the range and also it is aqua\",\n    \"text_review_upvote\": 19\n  }\n}\n```\n\nYou will get the similar response as follows:\n\n```json\n{\n  \"data\": {\n    \"predictor\": {\n      \"error\": null,\n      \"ok\": true,\n      \"predictions\": {\n        \"rating\": {\n          \"probability\": 0.4749999940395355,\n          \"rating\": 3,\n          \"stars\": \"⭐⭐⭐\"\n        },\n        \"recommend\": {\n          \"class_\": \"NOT RECOMMENDED\",\n          \"emoji\": \"👎\",\n          \"label\": 0,\n          \"probability\": 0.769\n        }\n      }\n    }\n  }\n}\n```\n\n### Model\n\nThe model that is being used here was build using tensorflow and keras `Subclassing` API and the architecture looks as follows:\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"./images/model-achitecture.png\" alt=\"model\"/\u003e\u003c/p\u003e\n\nThe implementation of the model in python code was as follows.\n\n```py\nclass APR(keras.Model):\n  def __init__(self):\n    super(APR, self).__init__()\n    # layers for bidirectional\n    forward_layer = keras.layers.GRU(\n      128, return_sequences=True, dropout=.5,\n      name=\"gru_forward_layer\"\n    )\n    backward_layer = keras.layers.LSTM(\n      128, return_sequences=True, dropout=.5,\n      go_backwards=True, name=\"lstm_backward_layer\"\n    )\n    self.embedding = keras.layers.Embedding(\n          vocab_size, 100,\n          input_length=max_words,\n          weights=[embedding_matrix],\n          trainable=True,\n          name = \"embedding_layer\"\n    )\n    self.bidirectional = keras.layers.Bidirectional(\n        forward_layer,\n        backward_layer = backward_layer,\n        name= \"bidirectional_layer\"\n    )\n    self.gru_layer = keras.layers.GRU(\n              512, return_sequences=True,\n              dropout=.5,\n              name= \"gru_layer\"\n      )\n    self.lstm_layer = keras.layers.LSTM(\n              512, return_sequences=True,\n              dropout=.5,\n              name=\"lstm_layer\"\n    )\n    self.fc_1 = keras.layers.Dense(512, activation=\"relu\", name=\"upvote_fc1\")\n    self.pooling_layer = keras.layers.GlobalAveragePooling1D(\n          name=\"average_pooling_layer\"\n    )\n    self.concatenate_layer = keras.layers.Concatenate(name=\"concatenate_layer_layer\")\n\n    self.dense_1 = keras.layers.Dense(64, activation='relu', name=\"dense_1\")\n    self.dropout_1 = keras.layers.Dropout(rate= .5, name=\"dropout_layer_0\")\n    self.dense_2 = keras.layers.Dense(512, activation='relu', name=\"dense_2\")\n    self.dropout_2 =  keras.layers.Dropout(rate= .5, name=\"dropout_layer_1\")\n    self.dense_3 = keras.layers.Dense(128, activation='relu', name=\"dense_3\")\n    self.dropout_3 = keras.layers.Dropout(rate= .5, name=\"dropout_layer_2\")\n    self.rating_output = keras.layers.Dense(5, activation='softmax', name=\"rating_output\")\n    self.recommend_output = keras.layers.Dense(1, activation='sigmoid', name=\"recommend_output\")\n\n  def call(self, inputs):\n    text, upvote = inputs\n    # Leaning the text features\n    x_1 = self.embedding(text)\n    x_1 = self.bidirectional(x_1)\n    x_1 = self.gru_layer(x_1)\n    x_1 = self.lstm_layer(x_1)\n    x_1 = self.pooling_layer(x_1)\n\n    # Learning the upvotes\n    x_2 = self.fc_1(upvote)\n\n    # concatenation\n    x = self.concatenate_layer([x_1, x_2])\n\n    # leaning combinned features\n    x = self.dense_1(self.dropout_1(x))\n    x = self.dense_2(self.dropout_2(x))\n    x = self.dense_3(self.dropout_3(x))\n\n    # outputs\n    rating = self.rating_output(x)\n    recommend = self.recommend_output(x)\n    return rating, recommend\n```\n\nThis models takes in two inputs which are:\n\n```\ntexts: String variable for the review body.\nupvotes: Positive Integer documenting the number of other customers who found this review positive.\n```\n\nAnd it outputs two outputs which are:\n\n```\nrecommended:\n    👍👎\n    Binary variable stating where the customer recommends the product where 1 is recommended, 0 is not recommended.\nrattings: ⭐⭐⭐⭐⭐\n    Positive Ordinal Integer variable for the product score granted by the customer from 1 Worst, to 5 Best.\n```\n\n### Top Techs used\n\n1. `TensorFlow + Keras` - For building a Deep learning model.\n2. `Google Colab` - As a workspace to train a model, by utilizing free GPU accelerations.\n3. `Python` - As a programming language.\n4. `Ariadne` - GraphQL package for python.\n5. `Flask` - To create a web server integrated with graphQL.\n\n### Notebooks\n\nThere are two notebooks that were used in this project the one for data preparation and the other one for model training.\n\n1. [Data Preparation](/notebooks/00_E_Commerce_Clothing_Reviews_Data_Prep.ipynb)\n2. [Model Training](\u003c/notebooks/01_Automatic_Product_Recommender_(APR).ipynb\u003e)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrispengari%2Fapr-dl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrispengari%2Fapr-dl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrispengari%2Fapr-dl/lists"}