From 8b3a8abbc2a4fad1646a456fbff210f6daee9627 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:38:39 +0000 Subject: [PATCH 01/23] issue #38: repo api-test, DESIGN.md --- api-test/DESIGN.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 api-test/DESIGN.md diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md new file mode 100644 index 0000000..aef4894 --- /dev/null +++ b/api-test/DESIGN.md @@ -0,0 +1,72 @@ +# Design of the Finesse Test Utility + +## Overview + +This tool simplifies the process of comparing different search engines and assessing their efficiency. It's designed to be straightforward, making it easy to understand and use. + +## How it Works + +* **Single command:** + * Users can enter commands with clear instructions to choose a search engine and specify a directory for analysis. + * Mandatory arguments: + * `--engine [argument]`: Pick a search engine. + * ai-lab: AI-Lab search returns up to 10 documents + * azure: Azure search has no returned documents limit + * static: Static search has no returned documents limit + * `path`: Point to the directory with files structured like Q&As in finesse-data. + * Optional argument: + * `--detailed`: Display the expected document and all the documents returned by the Finesse search +* **Accuracy score** + * The tool compares expected QnA pages with actual Finesse response pages. + * It calculates an accuracy score for each response based on the document's position in the results. + * Scores range from 0 (not in the top 10 documents) to 1.0 (the first document). + +* **Efficiency Calculation** + * Finesse's overall efficiency is measured by averaging the accuracy scores of all responses. + +## Example Command + +### Simple test + +```cmd +$ finesse-test --engine azure "/qna-tests" +Searching with Azure Search... + +File: "qna_2023-12-08_15" +Question: "Quels sont les numéros de téléphone pour les demandes de renseignements du public?" +Accuracy Score: 70% + +File: "qna_2023-12-08_17" +Question: "Quels sont les contacts pour les demandes de renseignements du public?" +Accuracy Score: 80% + +--- +Tested files: 2 +Finesse Overall Efficiency: 75% +``` + +### Detailed test + +```cmd +$ finesse-test --engine azure "/qna-tests" +Searching with Azure Search... + +File: "qna_2023-12-08_41" +Question: "Quels sont les numéros de téléphone pour les demandes de renseignements du public?" + +Pages returned: +1. Demandes de renseignements du public et des médias +2. 4.2 Matériel de multiplication de Rubus spp. des Pays-Bas ou de l'Allemagne +3. Pour les consommateurs +4. 3.9.2 En provenance de la zone continentale des États-Unis + +Expected Page: 3.9.2 En provenance de la zone continentale des États-Unis + +Accuracy Score: 70% + +--- +Tested files: 1 +Finesse Overall Efficiency: 70% +``` + +This example shows how the CLI Output of the tool, analyzing search results from Azure Search and providing an efficiency score for Finesse based on the accuracy of its responses. From b9cfd64340e9df4a0a00371fdc4a31b20555f2de Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:54:16 +0000 Subject: [PATCH 02/23] issue #38: typos --- api-test/DESIGN.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index aef4894..602ac6d 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -2,34 +2,42 @@ ## Overview -This tool simplifies the process of comparing different search engines and assessing their efficiency. It's designed to be straightforward, making it easy to understand and use. +This tool simplifies the process of comparing different search engines and +assessing their efficiency. It's designed to be straightforward, making it easy +to understand and use. ## How it Works * **Single command:** - * Users can enter commands with clear instructions to choose a search engine and specify a directory for analysis. + * Users can enter commands with clear instructions to choose a search engine + and specify a directory for analysis. * Mandatory arguments: * `--engine [argument]`: Pick a search engine. * ai-lab: AI-Lab search returns up to 10 documents * azure: Azure search has no returned documents limit * static: Static search has no returned documents limit - * `path`: Point to the directory with files structured like Q&As in finesse-data. + * `--path [argument]`: Point to the directory with files structured like Q&As in + finesse-data. * Optional argument: - * `--detailed`: Display the expected document and all the documents returned by the Finesse search + * `--detailed`: Display the expected document and all the documents returned + by the Finesse search * **Accuracy score** * The tool compares expected QnA pages with actual Finesse response pages. - * It calculates an accuracy score for each response based on the document's position in the results. - * Scores range from 0 (not in the top 10 documents) to 1.0 (the first document). + * It calculates an accuracy score for each response based on the document's + position in the results. + * Scores range from 0 (not in the top 10 documents) to 1.0 (the first + document). * **Efficiency Calculation** - * Finesse's overall efficiency is measured by averaging the accuracy scores of all responses. + * Finesse's overall efficiency is measured by averaging the accuracy scores of + all responses. ## Example Command ### Simple test ```cmd -$ finesse-test --engine azure "/qna-tests" +$ finesse-test --engine azure --path "/qna-tests" Searching with Azure Search... File: "qna_2023-12-08_15" @@ -48,7 +56,7 @@ Finesse Overall Efficiency: 75% ### Detailed test ```cmd -$ finesse-test --engine azure "/qna-tests" +$ finesse-test --engine azure --path "/qna-tests" --detailed Searching with Azure Search... File: "qna_2023-12-08_41" @@ -69,4 +77,6 @@ Tested files: 1 Finesse Overall Efficiency: 70% ``` -This example shows how the CLI Output of the tool, analyzing search results from Azure Search and providing an efficiency score for Finesse based on the accuracy of its responses. +This example shows how the CLI Output of the tool, analyzing search results from +Azure Search and providing an efficiency score for Finesse based on the accuracy +of its responses. From eff1cf669a3ea996ec0af8ff046579502166d9b8 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:54:01 +0000 Subject: [PATCH 03/23] issue #38: Ricky review on DESIGN.md --- api-test/DESIGN.md | 188 ++++++++++++++++++++++++++++++------------- api-test/diagram.png | Bin 0 -> 137234 bytes 2 files changed, 133 insertions(+), 55 deletions(-) create mode 100644 api-test/diagram.png diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index 602ac6d..c3dc0c2 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -1,82 +1,160 @@ # Design of the Finesse Test Utility +## Tools available + +There are tools that can integrate with Python or a script to accurately +calculate API statistics. Currently, the needs are to test the tool using JSON +files containing questions and their page origin in order to establish an +accuracy score. We also want to calculate request times and generate a +statistical summary of all this data. That being said, we plan to test the APIs +under different conditions in the near future. For example, with multiple +simultaneous users or under special conditions. That's why it's worth +researching tools, if they are scalable and well adapted with Python. + +### Decision + +The most promising tool we found was [**Locust**](https://github.com/locustio). +It seamlessly integrates with Python, making it a natural choice due to its +dependency availability. It offers several advantages, as outlined below. +However, **we decided to not use it** as its primary use case is to conduct +tests with identical repeatable requests involving simultaneous users or +machines and endpoints. Our specific testing requirements involve conducting +multiple tests with different headers each time, which deviates from the tool's +primary purpose of repeating the same test multiple times while adjusting user +load. Too much modification and work would be necessary to adapt our test +utility to Locust. Nevertheless, there is potential for future use where stress +and load testing involving repeated searches may be integrated. + +### Alternatives Considered + +#### Locust + +Locust is an open-source load testing framework in Python, allowing simulation +of many concurrent users making requests to a given system, and +providing detailed results on the performance and scalability of that system. + +Pros + +- Python dependency +- Built-in UI +- Easy integration with test scripts. +- Incorporate statistics, including median response time and request error + percentage +- Locust's versatility allows it to test any tool by allowing custom tests +- Locust is popular and open source, with support from major tech companies such +as Microsoft and Google +- Issues are actively managed on its GitHub repository +- Scalable, enabling easy testing of multiple scenarios with simultaneous users +or machines and endpoints + +Cons + +- Designed for scalability and to repeatable requests + +#### Apache Bench (ab) + +Apache Bench (ab) is a command-line tool for benchmarking HTTP servers. It is +included with the Apache HTTP Server package and is designed for simplicity and +ease of use. + +Pros + +- Simple to use. +- Good for basic testing. +- Easy integration with test scripts. + +Cons + +- May not be flexible enough for complex testing scenarios. +- Less performant for heavy loads or advanced testing. + +#### Siege + +Siege is a load testing and benchmarking tool that simulates multiple users +accessing a web server, enabling stress testing and performance evaluation. + +Pros + +- Supports multiple concurrent users, making it suitable for load testing. +- Allows for stress testing of web servers and applications. + +Cons + +- Lack of documentation, some arguments are not documented in their wiki. +- May have a steeper learning curve compared to simpler tools like Apache Bench. + ## Overview This tool simplifies the process of comparing different search engines and -assessing their efficiency. It's designed to be straightforward, making it easy +assessing their accuracy. It's designed to be straightforward, making it easy to understand and use. ## How it Works -* **Single command:** - * Users can enter commands with clear instructions to choose a search engine - and specify a directory for analysis. - * Mandatory arguments: - * `--engine [argument]`: Pick a search engine. - * ai-lab: AI-Lab search returns up to 10 documents - * azure: Azure search has no returned documents limit - * static: Static search has no returned documents limit - * `--path [argument]`: Point to the directory with files structured like Q&As in - finesse-data. - * Optional argument: - * `--detailed`: Display the expected document and all the documents returned - by the Finesse search -* **Accuracy score** - * The tool compares expected QnA pages with actual Finesse response pages. - * It calculates an accuracy score for each response based on the document's - position in the results. - * Scores range from 0 (not in the top 10 documents) to 1.0 (the first - document). - -* **Efficiency Calculation** - * Finesse's overall efficiency is measured by averaging the accuracy scores of - all responses. +- **Single command:** + - Users can enter commands with clear instructions to choose a search engine, + specify a directory for JSON files and specify the backend URL. + - Mandatory arguments: + - `--engine [search engine]`: Pick a search engine. + - `ai-lab` : AI-Lab search engine + - `azure`: Azure search engine + - `static`: Static search engine + - `--path [directory path]`: Point to the directory with files structured + - `--backend [base API URL]`: Point to the finesse-backend URL + with JSON files with the following properties: + - `score`: The score of the page. + - `crawl_id`: The unique identifier associated with the crawl table. + - `chunk_id`: The unique identifier of the chunk. + - `title`: The title of the page. + - `url`: The URL of the page. + - `text_content`: The main textual content of the item. + - `question`: The question to ask. + - `answer`: The response to the asked question. + - Optional argument: + - `--format [file type]`: + - `csv`: generate a CSV document + - `md`: generate a Markdown document, selected by default +- **Recursive** + - Search in all the folders in the directory to retrieve the JSON files +- **Accuracy score** + - The tool compares the expected page with the actual Finesse response pages. + - Calculates an accuracy score for each response based on its position in the + list of pages relative to the total number of pages in the list. 100% would + correspond of being at the top of the list, and 0% would mean not in the + list. +- **Round trip time** + - Measure round trip time of each request +- **Summary statistical value** + - Measure the average, minimum and maximal accuracy scores and round trip time + +## Diagram + +![Alt text](diagram.png) ## Example Command -### Simple test - ```cmd -$ finesse-test --engine azure --path "/qna-tests" +$finesse-test --engine azure --path "/qna-tests" -H "https://127.0.0.1" Searching with Azure Search... File: "qna_2023-12-08_15" -Question: "Quels sont les numéros de téléphone pour les demandes de renseignements du public?" +Question: "Quels sont les numéros de téléphone pour les demandes de r +enseignements du public?" Accuracy Score: 70% +Time: 875ms File: "qna_2023-12-08_17" Question: "Quels sont les contacts pour les demandes de renseignements du public?" Accuracy Score: 80% +Time: 786ms --- Tested files: 2 -Finesse Overall Efficiency: 75% -``` - -### Detailed test - -```cmd -$ finesse-test --engine azure --path "/qna-tests" --detailed -Searching with Azure Search... - -File: "qna_2023-12-08_41" -Question: "Quels sont les numéros de téléphone pour les demandes de renseignements du public?" - -Pages returned: -1. Demandes de renseignements du public et des médias -2. 4.2 Matériel de multiplication de Rubus spp. des Pays-Bas ou de l'Allemagne -3. Pour les consommateurs -4. 3.9.2 En provenance de la zone continentale des États-Unis - -Expected Page: 3.9.2 En provenance de la zone continentale des États-Unis - -Accuracy Score: 70% - ---- -Tested files: 1 -Finesse Overall Efficiency: 70% +Approximate round trip times in milli-seconds: + Minimum = 786, Maximum = 875, Average = 831ms +Approximate Finesse Accuracy Score: + Minimum = 70%, Maximum = 80%, Average = 75% ``` This example shows how the CLI Output of the tool, analyzing search results from -Azure Search and providing an efficiency score for Finesse based on the accuracy -of its responses. +Azure Search and providing an accuracy score for Finesse. diff --git a/api-test/diagram.png b/api-test/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a8aa3674381d655246f46bd8744ec2e45bf612 GIT binary patch literal 137234 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 Date: Thu, 8 Feb 2024 19:58:08 +0000 Subject: [PATCH 04/23] issue #38: locust test parser --- .gitignore | 3 ++ api-test/DESIGN.md | 4 +-- api-test/jsonreader.py | 47 ++++++++++++++++++++++++++++++ api-test/locustfile.py | 65 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 api-test/jsonreader.py create mode 100644 api-test/locustfile.py diff --git a/.gitignore b/.gitignore index a64fefc..b33c13e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ keys/ # Ignore Flask Sessions flask_session/ + +# Ignore local QnA json files +api-test/QnA diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index c3dc0c2..2a1a16d 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -114,8 +114,8 @@ to understand and use. - `--format [file type]`: - `csv`: generate a CSV document - `md`: generate a Markdown document, selected by default -- **Recursive** - - Search in all the folders in the directory to retrieve the JSON files +- **Many tests** + - Search all the JSON files in the directory - **Accuracy score** - The tool compares the expected page with the actual Finesse response pages. - Calculates an accuracy score for each response based on its position in the diff --git a/api-test/jsonreader.py b/api-test/jsonreader.py new file mode 100644 index 0000000..350a230 --- /dev/null +++ b/api-test/jsonreader.py @@ -0,0 +1,47 @@ +import json +from typing import Iterator, Dict +import os + +class JSONReader(Iterator): + "Read test data from JSON files using an iterator" + + def __init__(self, directory): + self.directory = directory + self.file_list = [f for f in os.listdir(directory) if f.endswith('.json')] + if not self.file_list: + raise FileNotFoundError(f"No JSON files found in the directory '{directory}'") + self.current_file_index = 0 + + def __iter__(self): + return self + + def __next__(self): + if self.current_file_index >= len(self.file_list): + raise StopIteration + + file_path = os.path.join(self.directory, self.file_list[self.current_file_index]) + + with open(file_path, 'r') as f: + data = json.load(f) + self.current_file_index += 1 + return data + + +class JSONDictReader(Iterator[Dict]): + "Read test data from JSON files using an iterator, returns rows as dicts" + + def __init__(self, directory): + self.reader = JSONReader(directory) + + def __iter__(self): + return self + + def __next__(self): + data = next(self.reader) + if isinstance(data, list): + return data[0] # Assuming each JSON file contains a list of dictionaries + elif isinstance(data, dict): + return data + else: + raise ValueError("Invalid JSON format") + diff --git a/api-test/locustfile.py b/api-test/locustfile.py new file mode 100644 index 0000000..a09c265 --- /dev/null +++ b/api-test/locustfile.py @@ -0,0 +1,65 @@ +import requests +from locust import HttpUser, task, events +from jsonreader import JSONReader +import os + +def is_host_up(host_url: str) -> bool: + health_check_endpoint = f"{host_url}/health" + try: + response = requests.get(health_check_endpoint) + return response.status_code == 200 + except requests.RequestException: + return False + +@events.init_command_line_parser.add_listener +def _(parser): + parser.add_argument("--engine", type=str, choices=["ai-lab", "azure", "static"], required=True, help="Pick a search engine.") + parser.add_argument("--path", type=str, required=True, help="Point to the directory with files structured") + parser.add_argument("--format", type=str, choices=["csv", "md"], default="md", help="Generate a CSV or Markdown document") + args = parser.parse_args() + + if not os.path.isdir(args.path): + parser.error(f"The directory '{args.path}' does not exist.") + + if not is_host_up(args.host): + parser.error(f"The backend URL '{args.host}' is either wrong or not up.") + +class FinesseUser(HttpUser): + @task + def search_accuracy(self): + # Reset the JSON itterator at the end of the itteration + try: + question_data = next(self.qna_reader) + except StopIteration: + self.qna_reader = JSONReader(self.path) + question_data = next(self.qna_reader) + print("Restarting test") + + question = question_data.get("question") + expected_page_title = question_data.get("title") + search_endpoint = f"{self.host}/search/{self.engine}" + response = self.client.get(search_endpoint, params={"query": question}) + if response.status_code == 200: + response_pages = response.text + print(response_pages) + + #accuracy = self.calculate_accuracy(response, expected_page_title) + #print(f"The accuracy of the question: {question} is {accuracy}%") + + def on_start(self): + self.qna_reader = JSONReader(self.path) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.path = self.environment.parsed_options.path + self.engine = self.environment.parsed_options.engine + self.format = self.environment.parsed_options.format + + def calculate_accuracy(self, response_pages: dict, expected_title: str) -> float: + response_titles: list[str] = [page.get("title") for page in response_pages] + if expected_title in response_titles: + index = response_titles.index(expected_title) + accuracy = 100 - (index / len(response_titles)) * 100 + else: + accuracy = 0 + return accuracy diff --git a/requirements.txt b/requirements.txt index 06a4db3..74babda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ git+https://github.com/ai-cfia/azure-db.git@main#subdirectory=azure-ai-search fuzzywuzzy python-Levenshtein git+https://github.com/ai-cfia/ailab-db@main +locust From be672c37021ac05323f33360ad5bbdba6442aa41 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Thu, 8 Feb 2024 21:16:07 +0000 Subject: [PATCH 05/23] issue #38: search post --- api-test/locustfile.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/api-test/locustfile.py b/api-test/locustfile.py index a09c265..2711617 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -2,6 +2,7 @@ from locust import HttpUser, task, events from jsonreader import JSONReader import os +import json def is_host_up(host_url: str) -> bool: health_check_endpoint = f"{host_url}/health" @@ -37,10 +38,16 @@ def search_accuracy(self): question = question_data.get("question") expected_page_title = question_data.get("title") - search_endpoint = f"{self.host}/search/{self.engine}" - response = self.client.get(search_endpoint, params={"query": question}) + + search_url = f"{self.host}/search/{self.engine}" + data = {'query': f'{question}'} + + # Headers + headers = {'Content-Type': 'application/json'} + + response = self.client.post(search_url, params={"query": question}, data=json.dumps(data), headers=headers) if response.status_code == 200: - response_pages = response.text + response_pages = response.json() print(response_pages) #accuracy = self.calculate_accuracy(response, expected_page_title) From 3f2b06395a35b0c0dc00c7b53d6a2c34700e1154 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 9 Feb 2024 20:15:36 +0000 Subject: [PATCH 06/23] issue #38: accuracy score calculation implemented --- api-test/jsonreader.py | 4 +- api-test/locustfile.py | 91 +++++++++++++++++++++++++++++------------- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/api-test/jsonreader.py b/api-test/jsonreader.py index 350a230..3c9ee4d 100644 --- a/api-test/jsonreader.py +++ b/api-test/jsonreader.py @@ -11,6 +11,7 @@ def __init__(self, directory): if not self.file_list: raise FileNotFoundError(f"No JSON files found in the directory '{directory}'") self.current_file_index = 0 + self.file_name = None # Initialize file_name attribute def __iter__(self): return self @@ -20,6 +21,7 @@ def __next__(self): raise StopIteration file_path = os.path.join(self.directory, self.file_list[self.current_file_index]) + self.file_name = self.file_list[self.current_file_index] # Update file_name attribute with open(file_path, 'r') as f: data = json.load(f) @@ -27,6 +29,7 @@ def __next__(self): return data + class JSONDictReader(Iterator[Dict]): "Read test data from JSON files using an iterator, returns rows as dicts" @@ -44,4 +47,3 @@ def __next__(self): return data else: raise ValueError("Invalid JSON format") - diff --git a/api-test/locustfile.py b/api-test/locustfile.py index 2711617..e34aec7 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -3,6 +3,8 @@ from jsonreader import JSONReader import os import json +from collections import namedtuple + def is_host_up(host_url: str) -> bool: health_check_endpoint = f"{host_url}/health" @@ -17,56 +19,91 @@ def _(parser): parser.add_argument("--engine", type=str, choices=["ai-lab", "azure", "static"], required=True, help="Pick a search engine.") parser.add_argument("--path", type=str, required=True, help="Point to the directory with files structured") parser.add_argument("--format", type=str, choices=["csv", "md"], default="md", help="Generate a CSV or Markdown document") + parser.add_argument("--once", action="store_true", default=False, help="Set this flag to make the accuracy test non-repeatable.") args = parser.parse_args() if not os.path.isdir(args.path): parser.error(f"The directory '{args.path}' does not exist.") if not is_host_up(args.host): - parser.error(f"The backend URL '{args.host}' is either wrong or not up.") + parser.error(f"The backend URL '{args.host}' is either wrong or down.") class FinesseUser(HttpUser): - @task + + AccuracyResult = namedtuple("AccuracyResult", ["position", "total_pages", "score"]) + + @task() def search_accuracy(self): - # Reset the JSON itterator at the end of the itteration try: - question_data = next(self.qna_reader) + json_data = next(self.qna_reader) except StopIteration: - self.qna_reader = JSONReader(self.path) - question_data = next(self.qna_reader) - print("Restarting test") - - question = question_data.get("question") - expected_page_title = question_data.get("title") + if not self.once: + # Reset variables + self.on_start() + json_data = next(self.qna_reader) + print("Restarting the running test") + else: + print("Stopping the running test") + self.environment.runner.quit() + question = json_data.get("question") + expected_page = json_data.copy() + del expected_page['question'] + del expected_page['answer'] + expected_url = json_data.get("url") + file_name = self.qna_reader.file_name search_url = f"{self.host}/search/{self.engine}" - data = {'query': f'{question}'} - - # Headers - headers = {'Content-Type': 'application/json'} + data = json.dumps({'query': f'{question}'}) + headers = { "Content-Type": "application/json" } + response_urls : list[str] = [] - response = self.client.post(search_url, params={"query": question}, data=json.dumps(data), headers=headers) + response = self.client.post(search_url, data=data, headers=headers) if response.status_code == 200: response_pages = response.json() - print(response_pages) + for page in response_pages: + del page['content'] + response_urls.append(page.get("url")) + accuracy_result = self.calculate_accuracy(response_urls, expected_url) + time_taken = response.elapsed.microseconds/1000 + self.qna_results[file_name] = { + "question": question, + "expected_page": expected_page, + "response_pages": response_pages, + "position": accuracy_result.position, + "total_pages": accuracy_result.total_pages, + "accuracy": accuracy_result.score, + "time": time_taken, + } - #accuracy = self.calculate_accuracy(response, expected_page_title) - #print(f"The accuracy of the question: {question} is {accuracy}%") + def calculate_accuracy(self, responses_url: list[str], expected_url: str) -> AccuracyResult: + position: int = 0 + total_pages: int = len(responses_url) + score: float = 0.0 + + if expected_url in responses_url: + position = responses_url.index(expected_url) + score = 1 - (position / total_pages) + + return self.AccuracyResult(position, total_pages, score) + + def log_data(self): + for key, value in self.qna_results.items(): + print("File:", key) + print("Question:", value.get("question")) + print(f'Accuracy Score: {value.get("accuracy")}%') + print(f'Time: {value.get("time")}ms') + print() def on_start(self): self.qna_reader = JSONReader(self.path) + self.qna_results = dict() + + def on_stop(self): + self.log_data() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.path = self.environment.parsed_options.path self.engine = self.environment.parsed_options.engine self.format = self.environment.parsed_options.format - - def calculate_accuracy(self, response_pages: dict, expected_title: str) -> float: - response_titles: list[str] = [page.get("title") for page in response_pages] - if expected_title in response_titles: - index = response_titles.index(expected_title) - accuracy = 100 - (index / len(response_titles)) * 100 - else: - accuracy = 0 - return accuracy + self.once = self.environment.parsed_options.once From bce3b93b3bf77a2a4b2780c8270aea78fc29843a Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:01:35 +0000 Subject: [PATCH 07/23] issue #38: dashboard implementation --- .devcontainer/devcontainer.json | 3 +++ api-test/locustfile.py | 2 +- requirements.txt | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index adc3cad..870eaf5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,5 +13,8 @@ "DavidAnson.vscode-markdownlint" ] } + }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} } } diff --git a/api-test/locustfile.py b/api-test/locustfile.py index e34aec7..081bb29 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -90,7 +90,7 @@ def log_data(self): for key, value in self.qna_results.items(): print("File:", key) print("Question:", value.get("question")) - print(f'Accuracy Score: {value.get("accuracy")}%') + print(f'Accuracy Score: {value.get("accuracy")*100}%') print(f'Time: {value.get("time")}ms') print() diff --git a/requirements.txt b/requirements.txt index 74babda..31bc7a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ fuzzywuzzy python-Levenshtein git+https://github.com/ai-cfia/ailab-db@main locust +locust-plugins[dashboards] From 4d5dc1a2da7591f9cc1fa1983974e4dfed30ae66 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:27:42 +0000 Subject: [PATCH 08/23] issue #38: csv and md functions added, refactoring the code --- .devcontainer/devcontainer.json | 5 +- .gitignore | 5 +- api-test/DESIGN.md | 91 ++++++++++-------------- api-test/__init__.py | 0 api-test/accuracy_functions.py | 106 ++++++++++++++++++++++++++++ api-test/locustfile.py | 99 ++++++++++---------------- api-test/requirements-test.txt | 2 + api-test/test_accuracy_functions.py | 39 ++++++++++ api-test/utils.py | 9 +++ locust.conf | 3 + requirements.txt | 3 +- 11 files changed, 242 insertions(+), 120 deletions(-) create mode 100644 api-test/__init__.py create mode 100644 api-test/accuracy_functions.py create mode 100644 api-test/requirements-test.txt create mode 100644 api-test/test_accuracy_functions.py create mode 100644 api-test/utils.py create mode 100644 locust.conf diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 870eaf5..54587cc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "Python 3", "image": "mcr.microsoft.com/devcontainers/python:3.11", - "postCreateCommand": "pip3 install --user -r requirements.txt", + "postCreateCommand": "pip3 install --user -r requirements.txt && gh extension install https://github.com/nektos/gh-act", "customizations": { "vscode": { "extensions": [ @@ -15,6 +15,7 @@ } }, "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": {} + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} } } diff --git a/.gitignore b/.gitignore index b33c13e..57e715c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,7 @@ keys/ flask_session/ # Ignore local QnA json files -api-test/QnA +QnA + +# Ignore output of api-test +api-test/output diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index 2a1a16d..ca1f76a 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -13,44 +13,23 @@ researching tools, if they are scalable and well adapted with Python. ### Decision -The most promising tool we found was [**Locust**](https://github.com/locustio). -It seamlessly integrates with Python, making it a natural choice due to its -dependency availability. It offers several advantages, as outlined below. -However, **we decided to not use it** as its primary use case is to conduct -tests with identical repeatable requests involving simultaneous users or -machines and endpoints. Our specific testing requirements involve conducting -multiple tests with different headers each time, which deviates from the tool's -primary purpose of repeating the same test multiple times while adjusting user -load. Too much modification and work would be necessary to adapt our test -utility to Locust. Nevertheless, there is potential for future use where stress -and load testing involving repeated searches may be integrated. +We've opted for Locust as our tool of choice. It's seamlessly compatible with +Python, making it a natural fit due to its easy integration. Locust is an +open-source load testing framework written in Python, designed to simulate +numerous machines sending requests to a given system. It provides detailed +insights into the system's performance and scalability. With its built-in UI and +straightforward integration with Python scripts, Locust is user-friendly and +accessible. It is popular and open source, with support from major tech +companies such as Microsoft and Google + +However, Locust's primary purpose is to conduct ongoing tests involving multiple +machines and endpoints simultaneously. Our specific requirement involves running +the accuracy test just once. Nevertheless, there's potential for future +integration, especially for stress and load testing scenarios that involve +repeated searches. ### Alternatives Considered -#### Locust - -Locust is an open-source load testing framework in Python, allowing simulation -of many concurrent users making requests to a given system, and -providing detailed results on the performance and scalability of that system. - -Pros - -- Python dependency -- Built-in UI -- Easy integration with test scripts. -- Incorporate statistics, including median response time and request error - percentage -- Locust's versatility allows it to test any tool by allowing custom tests -- Locust is popular and open source, with support from major tech companies such -as Microsoft and Google -- Issues are actively managed on its GitHub repository -- Scalable, enabling easy testing of multiple scenarios with simultaneous users -or machines and endpoints - -Cons - -- Designed for scalability and to repeatable requests - #### Apache Bench (ab) Apache Bench (ab) is a command-line tool for benchmarking HTTP servers. It is @@ -99,8 +78,9 @@ to understand and use. - `ai-lab` : AI-Lab search engine - `azure`: Azure search engine - `static`: Static search engine + - `llamaindex`: LlamaIndex search engine - `--path [directory path]`: Point to the directory with files structured - - `--backend [base API URL]`: Point to the finesse-backend URL + - `--host [API URL]`: Point to the finesse-backend URL with JSON files with the following properties: - `score`: The score of the page. - `crawl_id`: The unique identifier associated with the crawl table. @@ -114,8 +94,9 @@ to understand and use. - `--format [file type]`: - `csv`: generate a CSV document - `md`: generate a Markdown document, selected by default + - `--once`: go through all the json files and does not repeat - **Many tests** - - Search all the JSON files in the directory + - Test all the JSON files in the path directory - **Accuracy score** - The tool compares the expected page with the actual Finesse response pages. - Calculates an accuracy score for each response based on its position in the @@ -125,7 +106,7 @@ to understand and use. - **Round trip time** - Measure round trip time of each request - **Summary statistical value** - - Measure the average, minimum and maximal accuracy scores and round trip time + - Measure the average, median, minimum and maximal accuracy scores and round trip time ## Diagram @@ -134,26 +115,30 @@ to understand and use. ## Example Command ```cmd -$finesse-test --engine azure --path "/qna-tests" -H "https://127.0.0.1" +$locust --engine azure --path api-test/QnA/good_question --host https://finesse-guidance.ninebasetwo.xyz/api --once Searching with Azure Search... -File: "qna_2023-12-08_15" -Question: "Quels sont les numéros de téléphone pour les demandes de r -enseignements du public?" -Accuracy Score: 70% -Time: 875ms +File: qna_2023-12-08_36.json +Question: Quelle est la zone réglementée dans la ville de Vancouver à partir du 19 mars 2022? +Expected URL: https://inspection.canada.ca/protection-des-vegetaux/especes-envahissantes/directives/date/d-96-15/fra/1323854808025/1323854941807 +Accuracy Score: 50.0% +Time: 277.836ms -File: "qna_2023-12-08_17" -Question: "Quels sont les contacts pour les demandes de renseignements du public?" -Accuracy Score: 80% -Time: 786ms +File: qna_2023-12-08_19.json +Question: What are the requirements for inspections of fishing vessels? +Expected URL: https://inspection.canada.ca/importing-food-plants-or-animals/food-imports/foreign-systems/audits/report-of-a-virtual-assessment-of-spain/eng/1661449231959/1661449232916 +Accuracy Score: 0.0% +Time: 677.906ms +... + +--- +Tested on 21 files. +Time statistical summary: + Mean:429.0285238095238, Median:399.857, Maximum:889.38, Minimum:207.972 +Accuracy statistical summary: + Mean:0.3523809523809524, Median:0.0, Maximum:1.0, Minimum:0.0 --- -Tested files: 2 -Approximate round trip times in milli-seconds: - Minimum = 786, Maximum = 875, Average = 831ms -Approximate Finesse Accuracy Score: - Minimum = 70%, Maximum = 80%, Average = 75% ``` This example shows how the CLI Output of the tool, analyzing search results from diff --git a/api-test/__init__.py b/api-test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py new file mode 100644 index 0000000..7cacb0d --- /dev/null +++ b/api-test/accuracy_functions.py @@ -0,0 +1,106 @@ +import statistics +import datetime +import csv +import os +from collections import namedtuple + +OUTPUT_FOLDER = "./api-test/output" +AccuracyResult = namedtuple("AccuracyResult", ["position", "total_pages", "score"]) + +def calculate_accuracy(responses_url: list[str], expected_url: str) -> AccuracyResult: + position: int = 0 + total_pages: int = len(responses_url) + score: float = 0.0 + + if expected_url in responses_url: + position = responses_url.index(expected_url) + score = 1 - (position / total_pages) + + return AccuracyResult(position, total_pages, score) + +def save_to_markdown(test_data: dict, engine: str): + if not os.path.exists(OUTPUT_FOLDER): + os.makedirs(OUTPUT_FOLDER) + date_string = datetime.datetime.now().strftime("%Y-%m-%d") + file_name = f"test_{engine}_{date_string}.md" + output_file = os.path.join(OUTPUT_FOLDER, file_name) + with open(output_file, "w") as md_file: + md_file.write(f"# Test on the {engine} search engine: {date_string}\n\n") + md_file.write("## Test data table\n\n") + md_file.write("| 📄 File | 💬 Question | 📏 Accuracy Score | ⌛ Time |\n") + md_file.write("|--------------------|-------------------------------------------------------------------------------------------------------------------------|----------------|----------|\n") + for key, value in test_data.items(): + md_file.write(f"| {key} | [{value.get('question')}]({value.get('expected_page').get('url')})' | {value.get('accuracy')*100:.1f}% | {value.get('time')}ms |\n") + md_file.write("\n") + md_file.write(f"Tested on {len(test_data)} files.\n\n") + + time_stats, accuracy_stats = calculate_statistical_summary(test_data) + md_file.write("## Statistical summary\n\n") + md_file.write("| Statistic | Time | Accuracy score|\n") + md_file.write("|-----------------------|------------|---------|\n") + md_file.write(f"|Mean| {time_stats.get('Mean')}ms | {accuracy_stats.get('Mean')}% |\n") + md_file.write(f"|Median| {time_stats.get('Median')}ms | {accuracy_stats.get('Median')}% |\n") + md_file.write(f"|Maximum| {time_stats.get('Maximum')}ms | {accuracy_stats.get('Maximum')}% |\n") + md_file.write(f"|Minimum| {time_stats.get('Minimum')}ms | {accuracy_stats.get('Minimum')}% |\n") + +def save_to_csv(test_data: dict, engine: str): + if not os.path.exists(OUTPUT_FOLDER): + os.makedirs(OUTPUT_FOLDER) + date_string = datetime.datetime.now().strftime("%Y-%m-%d") + file_name = f"test_{engine}_{date_string}.csv" + output_file = os.path.join(OUTPUT_FOLDER, file_name) + with open(output_file, "w", newline="") as csv_file: + writer = csv.writer(csv_file) + writer.writerow(["File", "Question", "Accuracy Score", "Time"]) + for key, value in test_data.items(): + writer.writerow([ + key, + value.get("question"), + f"{value.get('accuracy')}", + f"{value.get('time')}" + ]) + writer.writerow([]) + + time_stats, accuracy_stats = calculate_statistical_summary(test_data) + writer.writerow(["Statistic", "Time", "Accuracy Score"]) + writer.writerow(["Mean", f"{time_stats.get('Mean')}", f"{accuracy_stats.get('Mean')}"]) + writer.writerow(["Median", f"{time_stats.get('Median')}", f"{accuracy_stats.get('Median')}"]) + writer.writerow(["Maximum", f"{time_stats.get('Maximum')}", f"{accuracy_stats.get('Maximum')}"]) + writer.writerow(["Minimum", f"{time_stats.get('Minimum')}", f"{accuracy_stats.get('Minimum')}"]) + +def log_data(test_data: dict): + for key, value in test_data.items(): + print("File:", key) + print("Question:", value.get("question")) + print("Expected URL:", value.get("expected_page").get("url")) + print(f'Accuracy Score: {value.get("accuracy")*100}%') + print(f'Time: {value.get("time")}ms') + print() + time_stats, accuracy_stats = calculate_statistical_summary(test_data) + print("---") + print(f"Tested on {len(test_data)} files.") + print("Time statistical summary:", end="\n ") + for key,value in time_stats.items(): + print(f"{key}:{value},", end=' ') + print("\nAccuracy statistical summary:", end="\n ") + for key,value in accuracy_stats.items(): + print(f"{key}:{value},", end=' ') + print("\n---") + + +def calculate_statistical_summary(test_data: dict) -> tuple[dict, dict]: + times = [result.get("time") for result in test_data.values()] + accuracies = [result.get("accuracy") for result in test_data.values()] + time_stats = { + "Mean": statistics.mean(times), + "Median": statistics.median(times), + "Maximum": max(times), + "Minimum": min(times), + } + accuracy_stats = { + "Mean": statistics.mean(accuracies), + "Median": statistics.median(accuracies), + "Maximum": max(accuracies), + "Minimum": min(accuracies), + } + return time_stats, accuracy_stats diff --git a/api-test/locustfile.py b/api-test/locustfile.py index 081bb29..dcccaf8 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -1,22 +1,13 @@ -import requests from locust import HttpUser, task, events from jsonreader import JSONReader import os import json -from collections import namedtuple - - -def is_host_up(host_url: str) -> bool: - health_check_endpoint = f"{host_url}/health" - try: - response = requests.get(health_check_endpoint) - return response.status_code == 200 - except requests.RequestException: - return False +from accuracy_functions import save_to_markdown, save_to_csv, log_data, calculate_accuracy +from utils import is_host_up @events.init_command_line_parser.add_listener def _(parser): - parser.add_argument("--engine", type=str, choices=["ai-lab", "azure", "static"], required=True, help="Pick a search engine.") + parser.add_argument("--engine", type=str, choices=["ai-lab", "azure", "static", "llamaindex"], required=True, help="Pick a search engine.") parser.add_argument("--path", type=str, required=True, help="Point to the directory with files structured") parser.add_argument("--format", type=str, choices=["csv", "md"], default="md", help="Generate a CSV or Markdown document") parser.add_argument("--once", action="store_true", default=False, help="Set this flag to make the accuracy test non-repeatable.") @@ -30,8 +21,6 @@ def _(parser): class FinesseUser(HttpUser): - AccuracyResult = namedtuple("AccuracyResult", ["position", "total_pages", "score"]) - @task() def search_accuracy(self): try: @@ -46,60 +35,46 @@ def search_accuracy(self): print("Stopping the running test") self.environment.runner.quit() - question = json_data.get("question") - expected_page = json_data.copy() - del expected_page['question'] - del expected_page['answer'] - expected_url = json_data.get("url") - file_name = self.qna_reader.file_name - search_url = f"{self.host}/search/{self.engine}" - data = json.dumps({'query': f'{question}'}) - headers = { "Content-Type": "application/json" } - response_urls : list[str] = [] - - response = self.client.post(search_url, data=data, headers=headers) - if response.status_code == 200: - response_pages = response.json() - for page in response_pages: - del page['content'] - response_urls.append(page.get("url")) - accuracy_result = self.calculate_accuracy(response_urls, expected_url) - time_taken = response.elapsed.microseconds/1000 - self.qna_results[file_name] = { - "question": question, - "expected_page": expected_page, - "response_pages": response_pages, - "position": accuracy_result.position, - "total_pages": accuracy_result.total_pages, - "accuracy": accuracy_result.score, - "time": time_taken, - } - - def calculate_accuracy(self, responses_url: list[str], expected_url: str) -> AccuracyResult: - position: int = 0 - total_pages: int = len(responses_url) - score: float = 0.0 - - if expected_url in responses_url: - position = responses_url.index(expected_url) - score = 1 - (position / total_pages) - - return self.AccuracyResult(position, total_pages, score) - - def log_data(self): - for key, value in self.qna_results.items(): - print("File:", key) - print("Question:", value.get("question")) - print(f'Accuracy Score: {value.get("accuracy")*100}%') - print(f'Time: {value.get("time")}ms') - print() + if self.engine in ["ai-lab", "azure", "static"]: + question = json_data.get("question") + expected_url = json_data.get("url") + file_name = self.qna_reader.file_name + search_url = f"{self.host}/search/{self.engine}" + data = json.dumps({'query': f'{question}'}) + headers = { "Content-Type": "application/json" } + response_urls : list[str] = [] + + response = self.client.post(search_url, data=data, headers=headers) + if response.status_code == 200: + response_pages = response.json() + for page in response_pages: + response_urls.append(page.get("url")) + accuracy_result = calculate_accuracy(response_urls, expected_url) + time_taken = response.elapsed.microseconds/1000 + + expected_page = json_data.copy() + del expected_page['question'] + del expected_page['answer'] + self.qna_results[file_name] = { + "question": question, + "expected_page": expected_page, + "response_pages": response_pages, + "position": accuracy_result.position, + "total_pages": accuracy_result.total_pages, + "accuracy": accuracy_result.score, + "time": time_taken, + } def on_start(self): self.qna_reader = JSONReader(self.path) self.qna_results = dict() def on_stop(self): - self.log_data() + log_data(self.qna_results) + if self.format == "md": + save_to_markdown(self.qna_results, self.engine) + elif self.format == "csv": + save_to_csv(self.qna_results, self.engine) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/api-test/requirements-test.txt b/api-test/requirements-test.txt new file mode 100644 index 0000000..5d94512 --- /dev/null +++ b/api-test/requirements-test.txt @@ -0,0 +1,2 @@ +locust + diff --git a/api-test/test_accuracy_functions.py b/api-test/test_accuracy_functions.py new file mode 100644 index 0000000..f410a09 --- /dev/null +++ b/api-test/test_accuracy_functions.py @@ -0,0 +1,39 @@ +import unittest +from accuracy_functions import calculate_accuracy, save_to_markdown, save_to_csv + +class TestFunctions(unittest.TestCase): + + def test_calculate_accuracy(self): + responses_url = ["http://example.com/page1", "http://example.com/page2", "http://example.com/page3"] + expected_url = "http://example.com/page2" + result = calculate_accuracy(responses_url, expected_url) + self.assertEqual(result.position, 1) + self.assertEqual(result.total_pages, 3) + self.assertAlmostEqual(result.score, 0.6666666666666667) + + def test_save_to_markdown(self): + test_data = { + "file1": { + "question": "What is Python?", + "expected_page": {"url": "http://example.com/page1"}, + "accuracy": 0.8, + "time": 100 + } + } + engine = "test_engine" + save_to_markdown(test_data, engine) + + def test_save_to_csv(self): + test_data = { + "file1": { + "question": "What is Python?", + "expected_page": {"url": "http://example.com/page1"}, + "accuracy": 0.8, + "time": 100 + } + } + engine = "test_engine" + save_to_csv(test_data, engine) + +if __name__ == "__main__": + unittest.main() diff --git a/api-test/utils.py b/api-test/utils.py new file mode 100644 index 0000000..28c9d6d --- /dev/null +++ b/api-test/utils.py @@ -0,0 +1,9 @@ +import requests + +def is_host_up(host_url: str) -> bool: + health_check_endpoint = f"{host_url}/health" + try: + response = requests.get(health_check_endpoint) + return response.status_code == 200 + except requests.RequestException: + return False diff --git a/locust.conf b/locust.conf new file mode 100644 index 0000000..ec5ce4d --- /dev/null +++ b/locust.conf @@ -0,0 +1,3 @@ +# Default settings of locust +locustfile = api-test/locustfile.py +headless = true diff --git a/requirements.txt b/requirements.txt index 31bc7a9..16fcea7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,4 @@ git+https://github.com/ai-cfia/azure-db.git@main#subdirectory=azure-ai-search fuzzywuzzy python-Levenshtein git+https://github.com/ai-cfia/ailab-db@main -locust -locust-plugins[dashboards] + From 64a784b8f32f429dd0271628b78710702e50e25b Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:42:42 +0000 Subject: [PATCH 09/23] issue #38: updated DESIGN.md --- api-test/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index ca1f76a..28ed434 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -1,4 +1,4 @@ -# Design of the Finesse Test Utility +# Design of the Finesse Benchmark Tool ## Tools available From 68c9788adf52ad064b3e53e612af6ef3cc2071ba Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:45:57 +0000 Subject: [PATCH 10/23] issue #38: Updated unittest --- api-test/test_accuracy_functions.py | 32 ++++------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/api-test/test_accuracy_functions.py b/api-test/test_accuracy_functions.py index f410a09..f555194 100644 --- a/api-test/test_accuracy_functions.py +++ b/api-test/test_accuracy_functions.py @@ -4,36 +4,12 @@ class TestFunctions(unittest.TestCase): def test_calculate_accuracy(self): - responses_url = ["http://example.com/page1", "http://example.com/page2", "http://example.com/page3"] + responses_url = ["http://example.com/page1", "http://example.com/page2", "http://example.com/page3", "http://example.com/page4"] expected_url = "http://example.com/page2" result = calculate_accuracy(responses_url, expected_url) self.assertEqual(result.position, 1) - self.assertEqual(result.total_pages, 3) - self.assertAlmostEqual(result.score, 0.6666666666666667) - - def test_save_to_markdown(self): - test_data = { - "file1": { - "question": "What is Python?", - "expected_page": {"url": "http://example.com/page1"}, - "accuracy": 0.8, - "time": 100 - } - } - engine = "test_engine" - save_to_markdown(test_data, engine) - - def test_save_to_csv(self): - test_data = { - "file1": { - "question": "What is Python?", - "expected_page": {"url": "http://example.com/page1"}, - "accuracy": 0.8, - "time": 100 - } - } - engine = "test_engine" - save_to_csv(test_data, engine) - + self.assertEqual(result.total_pages, 4) + self.assertEqual(result.score, 0.75) + if __name__ == "__main__": unittest.main() From debe169d077e70475a2d0e9ffcacc00ad608777c Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:49:00 +0000 Subject: [PATCH 11/23] issue #38: Removed unused modules --- api-test/test_accuracy_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-test/test_accuracy_functions.py b/api-test/test_accuracy_functions.py index f555194..cd355eb 100644 --- a/api-test/test_accuracy_functions.py +++ b/api-test/test_accuracy_functions.py @@ -1,5 +1,5 @@ import unittest -from accuracy_functions import calculate_accuracy, save_to_markdown, save_to_csv +from accuracy_functions import calculate_accuracy class TestFunctions(unittest.TestCase): @@ -10,6 +10,6 @@ def test_calculate_accuracy(self): self.assertEqual(result.position, 1) self.assertEqual(result.total_pages, 4) self.assertEqual(result.score, 0.75) - + if __name__ == "__main__": unittest.main() From b7f0f0d90abcf80dc5e3d72b86dded56c9fea816 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:24:46 +0000 Subject: [PATCH 12/23] issue #38: new accuracy calculation --- api-test/accuracy_functions.py | 27 +++++++++++++++++---------- api-test/locustfile.py | 6 +++--- api-test/test_accuracy_functions.py | 9 +++++++-- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py index 7cacb0d..701e97f 100644 --- a/api-test/accuracy_functions.py +++ b/api-test/accuracy_functions.py @@ -8,15 +8,22 @@ AccuracyResult = namedtuple("AccuracyResult", ["position", "total_pages", "score"]) def calculate_accuracy(responses_url: list[str], expected_url: str) -> AccuracyResult: - position: int = 0 - total_pages: int = len(responses_url) - score: float = 0.0 + position: int = 0 + total_pages: int = len(responses_url) + score: float = 0.0 + expected_number = int(expected_url.split('/')[-1]) - if expected_url in responses_url: - position = responses_url.index(expected_url) + if expected_number == 0: + return AccuracyResult(position, total_pages, score) + + for idx, response_url in enumerate(responses_url): + response_number = int(response_url.split('/')[-1]) + if response_number == expected_number: + position = idx + 1 score = 1 - (position / total_pages) + break - return AccuracyResult(position, total_pages, score) + return AccuracyResult(position, total_pages, score) def save_to_markdown(test_data: dict, engine: str): if not os.path.exists(OUTPUT_FOLDER): @@ -38,10 +45,10 @@ def save_to_markdown(test_data: dict, engine: str): md_file.write("## Statistical summary\n\n") md_file.write("| Statistic | Time | Accuracy score|\n") md_file.write("|-----------------------|------------|---------|\n") - md_file.write(f"|Mean| {time_stats.get('Mean')}ms | {accuracy_stats.get('Mean')}% |\n") - md_file.write(f"|Median| {time_stats.get('Median')}ms | {accuracy_stats.get('Median')}% |\n") - md_file.write(f"|Maximum| {time_stats.get('Maximum')}ms | {accuracy_stats.get('Maximum')}% |\n") - md_file.write(f"|Minimum| {time_stats.get('Minimum')}ms | {accuracy_stats.get('Minimum')}% |\n") + md_file.write(f"|Mean| {time_stats.get('Mean')}ms | {accuracy_stats.get('Mean')*100}% |\n") + md_file.write(f"|Median| {time_stats.get('Median')}ms | {accuracy_stats.get('Median')*100}% |\n") + md_file.write(f"|Maximum| {time_stats.get('Maximum')}ms | {accuracy_stats.get('Maximum')*100}% |\n") + md_file.write(f"|Minimum| {time_stats.get('Minimum')}ms | {accuracy_stats.get('Minimum')*100}% |\n") def save_to_csv(test_data: dict, engine: str): if not os.path.exists(OUTPUT_FOLDER): diff --git a/api-test/locustfile.py b/api-test/locustfile.py index dcccaf8..70c68b1 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -42,14 +42,14 @@ def search_accuracy(self): search_url = f"{self.host}/search/{self.engine}" data = json.dumps({'query': f'{question}'}) headers = { "Content-Type": "application/json" } - response_urls : list[str] = [] + response_url : list[str] = [] response = self.client.post(search_url, data=data, headers=headers) if response.status_code == 200: response_pages = response.json() for page in response_pages: - response_urls.append(page.get("url")) - accuracy_result = calculate_accuracy(response_urls, expected_url) + response_url.append(page.get("url")) + accuracy_result = calculate_accuracy(response_url, expected_url) time_taken = response.elapsed.microseconds/1000 expected_page = json_data.copy() diff --git a/api-test/test_accuracy_functions.py b/api-test/test_accuracy_functions.py index cd355eb..afe44eb 100644 --- a/api-test/test_accuracy_functions.py +++ b/api-test/test_accuracy_functions.py @@ -4,8 +4,13 @@ class TestFunctions(unittest.TestCase): def test_calculate_accuracy(self): - responses_url = ["http://example.com/page1", "http://example.com/page2", "http://example.com/page3", "http://example.com/page4"] - expected_url = "http://example.com/page2" + responses_url = [ + "https://inspection.canada.ca/exporting-food-plants-or-animals/food-exports/food-specific-export-requirements/meat/crfpcp/eng/1434119937443/1434120400252", + "https://inspection.canada.ca/protection-des-vegetaux/especes-envahissantes/directives/date/d-08-04/fra/1323752901318/1323753612811", + "https://inspection.canada.ca/varietes-vegetales/vegetaux-a-caracteres-nouveaux/demandeurs/directive-94-08/documents-sur-la-biologie/lens-culinaris-medikus-lentille-/fra/1330978380871/1330978449837", + "https://inspection.canada.ca/protection-des-vegetaux/especes-envahissantes/directives/date/d-96-15/fra/1323854808025/1323854941807" + ] + expected_url = "https://inspection.canada.ca/exporting-food-plants-or-animals/food-exports/food-specific-export-requirements/meat/crfpcp/eng/1434119937443/1434120400252" result = calculate_accuracy(responses_url, expected_url) self.assertEqual(result.position, 1) self.assertEqual(result.total_pages, 4) From 2b9dca638444e995c1aa374c99ee6830c5c1f025 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:30:06 +0000 Subject: [PATCH 13/23] issue #38: top score fix --- api-test/accuracy_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py index 701e97f..48881b7 100644 --- a/api-test/accuracy_functions.py +++ b/api-test/accuracy_functions.py @@ -19,8 +19,8 @@ def calculate_accuracy(responses_url: list[str], expected_url: str) -> AccuracyR for idx, response_url in enumerate(responses_url): response_number = int(response_url.split('/')[-1]) if response_number == expected_number: - position = idx + 1 score = 1 - (position / total_pages) + position = idx + 1 break return AccuracyResult(position, total_pages, score) From 90a941402935f9471d972791fbd78bf04c98f8d0 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:13:10 +0000 Subject: [PATCH 14/23] issue #38: top argument added --- api-test/DESIGN.md | 7 ++++--- api-test/accuracy_functions.py | 7 ++----- api-test/locustfile.py | 8 +++++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index 28ed434..2f19cdc 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -92,9 +92,10 @@ to understand and use. - `answer`: The response to the asked question. - Optional argument: - `--format [file type]`: - - `csv`: generate a CSV document - - `md`: generate a Markdown document, selected by default - - `--once`: go through all the json files and does not repeat + - `csv`: Generate a CSV document + - `md`: Generate a Markdown document, selected by default + - `--once`: Go through all the json files and does not repeat + - `--top`: Limit the number of results returned by the search engine - **Many tests** - Test all the JSON files in the path directory - **Accuracy score** diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py index 48881b7..f84ced7 100644 --- a/api-test/accuracy_functions.py +++ b/api-test/accuracy_functions.py @@ -13,14 +13,11 @@ def calculate_accuracy(responses_url: list[str], expected_url: str) -> AccuracyR score: float = 0.0 expected_number = int(expected_url.split('/')[-1]) - if expected_number == 0: - return AccuracyResult(position, total_pages, score) - for idx, response_url in enumerate(responses_url): response_number = int(response_url.split('/')[-1]) if response_number == expected_number: + position = idx score = 1 - (position / total_pages) - position = idx + 1 break return AccuracyResult(position, total_pages, score) @@ -91,7 +88,7 @@ def log_data(test_data: dict): print(f"{key}:{value},", end=' ') print("\nAccuracy statistical summary:", end="\n ") for key,value in accuracy_stats.items(): - print(f"{key}:{value},", end=' ') + print(f"{key}:{value*100}%,", end=' ') print("\n---") diff --git a/api-test/locustfile.py b/api-test/locustfile.py index 70c68b1..ed0960c 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -11,6 +11,7 @@ def _(parser): parser.add_argument("--path", type=str, required=True, help="Point to the directory with files structured") parser.add_argument("--format", type=str, choices=["csv", "md"], default="md", help="Generate a CSV or Markdown document") parser.add_argument("--once", action="store_true", default=False, help="Set this flag to make the accuracy test non-repeatable.") + parser.add_argument("--top", type=str, default = 100, help="Set this number to limit the number of results returned by the search engine.") args = parser.parse_args() if not os.path.isdir(args.path): @@ -39,12 +40,12 @@ def search_accuracy(self): question = json_data.get("question") expected_url = json_data.get("url") file_name = self.qna_reader.file_name - search_url = f"{self.host}/search/{self.engine}" + response_url : list[str] = [] + search_url = f"{self.host}/search/{self.engine}?top={self.top}" data = json.dumps({'query': f'{question}'}) headers = { "Content-Type": "application/json" } - response_url : list[str] = [] - response = self.client.post(search_url, data=data, headers=headers) + if response.status_code == 200: response_pages = response.json() for page in response_pages: @@ -82,3 +83,4 @@ def __init__(self, *args, **kwargs): self.engine = self.environment.parsed_options.engine self.format = self.environment.parsed_options.format self.once = self.environment.parsed_options.once + self.top = self.environment.parsed_options.top From dd28838a8a5ddf2ed7e3a710c9c68872044983d9 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:16:53 +0000 Subject: [PATCH 15/23] issue #38: test reviewed --- api-test/test_accuracy_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-test/test_accuracy_functions.py b/api-test/test_accuracy_functions.py index afe44eb..3176b4c 100644 --- a/api-test/test_accuracy_functions.py +++ b/api-test/test_accuracy_functions.py @@ -10,7 +10,7 @@ def test_calculate_accuracy(self): "https://inspection.canada.ca/varietes-vegetales/vegetaux-a-caracteres-nouveaux/demandeurs/directive-94-08/documents-sur-la-biologie/lens-culinaris-medikus-lentille-/fra/1330978380871/1330978449837", "https://inspection.canada.ca/protection-des-vegetaux/especes-envahissantes/directives/date/d-96-15/fra/1323854808025/1323854941807" ] - expected_url = "https://inspection.canada.ca/exporting-food-plants-or-animals/food-exports/food-specific-export-requirements/meat/crfpcp/eng/1434119937443/1434120400252" + expected_url = "https://inspection.canada.ca/protection-des-vegetaux/especes-envahissantes/directives/date/d-08-04/fra/1323752901318/1323753612811" result = calculate_accuracy(responses_url, expected_url) self.assertEqual(result.position, 1) self.assertEqual(result.total_pages, 4) From c05b848a3b697f7c1a94764d835810922137e00f Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:09:47 +0000 Subject: [PATCH 16/23] issue #38: reviewed url comparison --- api-test/accuracy_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py index f84ced7..5e0e9cc 100644 --- a/api-test/accuracy_functions.py +++ b/api-test/accuracy_functions.py @@ -11,10 +11,10 @@ def calculate_accuracy(responses_url: list[str], expected_url: str) -> AccuracyR position: int = 0 total_pages: int = len(responses_url) score: float = 0.0 - expected_number = int(expected_url.split('/')[-1]) + expected_number = int(expected_url.split('/')[-2]) for idx, response_url in enumerate(responses_url): - response_number = int(response_url.split('/')[-1]) + response_number = int(response_url.split('/')[-2]) if response_number == expected_number: position = idx score = 1 - (position / total_pages) From 6a1f617ffdbd1d22df0c31c7d2db63da56c39699 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:44:42 +0000 Subject: [PATCH 17/23] issue #38: round digits --- api-test/accuracy_functions.py | 17 +++++++++-------- api-test/locustfile.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py index 5e0e9cc..c4709fa 100644 --- a/api-test/accuracy_functions.py +++ b/api-test/accuracy_functions.py @@ -18,6 +18,7 @@ def calculate_accuracy(responses_url: list[str], expected_url: str) -> AccuracyR if response_number == expected_number: position = idx score = 1 - (position / total_pages) + score= round(score, 2) break return AccuracyResult(position, total_pages, score) @@ -96,15 +97,15 @@ def calculate_statistical_summary(test_data: dict) -> tuple[dict, dict]: times = [result.get("time") for result in test_data.values()] accuracies = [result.get("accuracy") for result in test_data.values()] time_stats = { - "Mean": statistics.mean(times), - "Median": statistics.median(times), - "Maximum": max(times), - "Minimum": min(times), + "Mean": round(statistics.mean(times), 3), + "Median": round(statistics.median(times), 3), + "Maximum": round(max(times), 3), + "Minimum": round(min(times), 3), } accuracy_stats = { - "Mean": statistics.mean(accuracies), - "Median": statistics.median(accuracies), - "Maximum": max(accuracies), - "Minimum": min(accuracies), + "Mean": round(statistics.mean(accuracies), 2), + "Median": round(statistics.median(accuracies), 2), + "Maximum": round(max(accuracies), 2), + "Minimum": round(min(accuracies), 2), } return time_stats, accuracy_stats diff --git a/api-test/locustfile.py b/api-test/locustfile.py index ed0960c..5f59b8b 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -51,7 +51,7 @@ def search_accuracy(self): for page in response_pages: response_url.append(page.get("url")) accuracy_result = calculate_accuracy(response_url, expected_url) - time_taken = response.elapsed.microseconds/1000 + time_taken = round(response.elapsed.microseconds/1000,3) expected_page = json_data.copy() del expected_page['question'] From 127cbb0d6dc12ebfb3264170e7c81d793f48767f Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:18:14 +0000 Subject: [PATCH 18/23] issue #38: devcontainer initial --- .devcontainer/devcontainer.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 54587cc..adc3cad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "Python 3", "image": "mcr.microsoft.com/devcontainers/python:3.11", - "postCreateCommand": "pip3 install --user -r requirements.txt && gh extension install https://github.com/nektos/gh-act", + "postCreateCommand": "pip3 install --user -r requirements.txt", "customizations": { "vscode": { "extensions": [ @@ -13,9 +13,5 @@ "DavidAnson.vscode-markdownlint" ] } - }, - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, - "ghcr.io/devcontainers/features/github-cli:1": {} } } From 8a9c899873c5fe643359f70aab4a23f1f76a862a Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:19:52 +0000 Subject: [PATCH 19/23] issue #38: Remove JSONDict --- api-test/jsonreader.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/api-test/jsonreader.py b/api-test/jsonreader.py index 3c9ee4d..49e4140 100644 --- a/api-test/jsonreader.py +++ b/api-test/jsonreader.py @@ -27,23 +27,3 @@ def __next__(self): data = json.load(f) self.current_file_index += 1 return data - - - -class JSONDictReader(Iterator[Dict]): - "Read test data from JSON files using an iterator, returns rows as dicts" - - def __init__(self, directory): - self.reader = JSONReader(directory) - - def __iter__(self): - return self - - def __next__(self): - data = next(self.reader) - if isinstance(data, list): - return data[0] # Assuming each JSON file contains a list of dictionaries - elif isinstance(data, dict): - return data - else: - raise ValueError("Invalid JSON format") From 2784d50202f97418e56c4aefa675cfc04cc1d65c Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:21:20 +0000 Subject: [PATCH 20/23] issue #38: Rename utils to host --- api-test/{utils.py => host.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api-test/{utils.py => host.py} (100%) diff --git a/api-test/utils.py b/api-test/host.py similarity index 100% rename from api-test/utils.py rename to api-test/host.py From 13f9c0dcce6deec7abfa9570b61801c46f245383 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:12:28 +0000 Subject: [PATCH 21/23] issue #38: standard deviation --- api-test/accuracy_functions.py | 4 ++++ api-test/jsonreader.py | 2 +- api-test/locustfile.py | 8 +++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api-test/accuracy_functions.py b/api-test/accuracy_functions.py index c4709fa..8403aa5 100644 --- a/api-test/accuracy_functions.py +++ b/api-test/accuracy_functions.py @@ -45,6 +45,7 @@ def save_to_markdown(test_data: dict, engine: str): md_file.write("|-----------------------|------------|---------|\n") md_file.write(f"|Mean| {time_stats.get('Mean')}ms | {accuracy_stats.get('Mean')*100}% |\n") md_file.write(f"|Median| {time_stats.get('Median')}ms | {accuracy_stats.get('Median')*100}% |\n") + md_file.write(f"|Standard Deviation| {time_stats.get('Standard Deviation')}ms | {accuracy_stats.get('Standard Deviation')*100}% |\n") md_file.write(f"|Maximum| {time_stats.get('Maximum')}ms | {accuracy_stats.get('Maximum')*100}% |\n") md_file.write(f"|Minimum| {time_stats.get('Minimum')}ms | {accuracy_stats.get('Minimum')*100}% |\n") @@ -70,6 +71,7 @@ def save_to_csv(test_data: dict, engine: str): writer.writerow(["Statistic", "Time", "Accuracy Score"]) writer.writerow(["Mean", f"{time_stats.get('Mean')}", f"{accuracy_stats.get('Mean')}"]) writer.writerow(["Median", f"{time_stats.get('Median')}", f"{accuracy_stats.get('Median')}"]) + writer.writerow(["Standard Deviation", f"{time_stats.get('Standard Deviation')}", f"{accuracy_stats.get('Standard Deviation')}"]) writer.writerow(["Maximum", f"{time_stats.get('Maximum')}", f"{accuracy_stats.get('Maximum')}"]) writer.writerow(["Minimum", f"{time_stats.get('Minimum')}", f"{accuracy_stats.get('Minimum')}"]) @@ -99,12 +101,14 @@ def calculate_statistical_summary(test_data: dict) -> tuple[dict, dict]: time_stats = { "Mean": round(statistics.mean(times), 3), "Median": round(statistics.median(times), 3), + "Standard Deviation": round(statistics.stdev(times), 3), "Maximum": round(max(times), 3), "Minimum": round(min(times), 3), } accuracy_stats = { "Mean": round(statistics.mean(accuracies), 2), "Median": round(statistics.median(accuracies), 2), + "Standard Deviation": round(statistics.stdev(accuracies), 2), "Maximum": round(max(accuracies), 2), "Minimum": round(min(accuracies), 2), } diff --git a/api-test/jsonreader.py b/api-test/jsonreader.py index 49e4140..2c4c23f 100644 --- a/api-test/jsonreader.py +++ b/api-test/jsonreader.py @@ -1,5 +1,5 @@ import json -from typing import Iterator, Dict +from typing import Iterator import os class JSONReader(Iterator): diff --git a/api-test/locustfile.py b/api-test/locustfile.py index 5f59b8b..46f200d 100644 --- a/api-test/locustfile.py +++ b/api-test/locustfile.py @@ -3,7 +3,10 @@ import os import json from accuracy_functions import save_to_markdown, save_to_csv, log_data, calculate_accuracy -from utils import is_host_up +from host import is_host_up + +class NoTestDataError(Exception): + """Raised when all requests have failed and there is no test data""" @events.init_command_line_parser.add_listener def _(parser): @@ -71,6 +74,9 @@ def on_start(self): self.qna_results = dict() def on_stop(self): + if not self.qna_results: + raise NoTestDataError + log_data(self.qna_results) if self.format == "md": save_to_markdown(self.qna_results, self.engine) From 15d58f03c80a493cd81c4bc53206b037a1795be9 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:19:38 +0000 Subject: [PATCH 22/23] issue #38: DESIGN.md updated --- api-test/DESIGN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index 2f19cdc..b5467f9 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -136,9 +136,9 @@ Time: 677.906ms --- Tested on 21 files. Time statistical summary: - Mean:429.0285238095238, Median:399.857, Maximum:889.38, Minimum:207.972 + Mean:429, Median:400, Standard Deviation:150 Maximum:889, Minimum:208 Accuracy statistical summary: - Mean:0.3523809523809524, Median:0.0, Maximum:1.0, Minimum:0.0 + Mean:0.35, Median:0.0, Standard Deviation:0.25, Maximum:1.0, Minimum:0.0 --- ``` From 14f03ef4f7488f8e286075b22b5bb33b991c5850 Mon Sep 17 00:00:00 2001 From: Ibrahim Kabir <117961703+ibrahim-kabir@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:21:20 +0000 Subject: [PATCH 23/23] issue #38: Update DESIGN.md --- api-test/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-test/DESIGN.md b/api-test/DESIGN.md index b5467f9..9dca800 100644 --- a/api-test/DESIGN.md +++ b/api-test/DESIGN.md @@ -107,7 +107,7 @@ to understand and use. - **Round trip time** - Measure round trip time of each request - **Summary statistical value** - - Measure the average, median, minimum and maximal accuracy scores and round trip time + - Measure the average, median, standard deviation, minimum and maximal accuracy scores and round trip time ## Diagram