diff --git a/.gitignore b/.gitignore index 4f0be98..c5bd65c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ docker .idea -.codemie \ No newline at end of file +.codemie + +.vscode + +data/ml_models/ +phase1_datasets/ \ No newline at end of file diff --git a/apiGateway/src/main/java/com/library/apiGateway/configs/SecurityConfig.java b/apiGateway/src/main/java/com/library/apiGateway/configs/SecurityConfig.java index d87a119..eb1f949 100644 --- a/apiGateway/src/main/java/com/library/apiGateway/configs/SecurityConfig.java +++ b/apiGateway/src/main/java/com/library/apiGateway/configs/SecurityConfig.java @@ -9,9 +9,6 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; -import java.util.List; - @Configuration public class SecurityConfig { @@ -36,19 +33,42 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .authorizeHttpRequests(authorize -> authorize .requestMatchers(freeResourceUrls).permitAll() .anyRequest().authenticated()) - .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .build(); } + + /** + * CORS configuration for API Gateway. + * Frontend makes requests to the gateway, which proxies to internal services. + * Swagger UI on the gateway also needs to be allowed. + * Services do NOT need their own CORS config - this is the single point of CORS handling. + */ @Bean - CorsConfigurationSource corsConfigurationSource() { + public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("*")); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(List.of("*")); + + // Allow frontend origins + configuration.setAllowedOriginPatterns(java.util.List.of( + "http://localhost:4200", // Angular dev + "http://127.0.0.1:4200" + )); + + // Allow all HTTP methods + configuration.setAllowedMethods(java.util.List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + + // Allow all headers (including Authorization for JWT) + configuration.setAllowedHeaders(java.util.List.of("*")); + + // CRITICAL: Allow credentials (JWT tokens in Authorization header) + configuration.setAllowCredentials(true); + + // Cache preflight for 1 hour + configuration.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; diff --git a/book-bazaar/package-lock.json b/book-bazaar/package-lock.json index f61482b..8b881f6 100644 --- a/book-bazaar/package-lock.json +++ b/book-bazaar/package-lock.json @@ -441,7 +441,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.11.tgz", "integrity": "sha512-+zcP6eq9+h6f09rZWHNIj2nap9P6S38mm75/WjdGZbl1BJy7vaASDnr4fwXKi2JvTyap/vj6mMuadFXEivavPw==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -492,7 +491,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz", "integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -509,7 +507,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz", "integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -523,7 +520,6 @@ "integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -556,7 +552,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz", "integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -582,7 +577,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz", "integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -618,7 +612,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz", "integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -641,7 +634,6 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.10.tgz", "integrity": "sha512-Z03cfH1jgQ7XMDJj4R8qAGqivcvhdG3wYBwaiN1K1ODBgPhbFKNeD4stKqYp7xBNtswmM2O2jMxrL/Djwju4Gg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -686,7 +678,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1637,7 +1628,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3870,7 +3860,6 @@ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4242,7 +4231,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -5339,7 +5327,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6305,8 +6292,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jiti": { "version": "2.6.1", @@ -6400,7 +6386,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -6883,7 +6868,6 @@ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.1.tgz", "integrity": "sha512-bZt6fQj/TLBAmivXSxSlqAJxBx/knNZDQGJIW4ensGYGN4N6tUKV8Zj3Y7/LOV8eIpvWsvqV70fbACihK8Ze0Q==", "license": "Apache-2.0", - "peer": true, "workspaces": [ "test" ] @@ -7143,7 +7127,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -8514,7 +8497,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8819,7 +8801,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8876,7 +8857,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9725,8 +9705,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "3.1.0", @@ -9764,7 +9743,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9951,7 +9929,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10355,7 +10332,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/book-bazaar/src/app/components/book-details/book-details.css b/book-bazaar/src/app/components/book-details/book-details.css index f806e9c..36e3088 100644 --- a/book-bazaar/src/app/components/book-details/book-details.css +++ b/book-bazaar/src/app/components/book-details/book-details.css @@ -32,6 +32,29 @@ outline: none; } +/* Scrollable similar books container */ +.similar-books-scroll { + scrollbar-width: thin; + scrollbar-color: rgba(156, 163, 175, 0.5) transparent; +} + +.similar-books-scroll::-webkit-scrollbar { + width: 6px; +} + +.similar-books-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.similar-books-scroll::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.5); + border-radius: 3px; +} + +.similar-books-scroll::-webkit-scrollbar-thumb:hover { + background: rgba(107, 114, 128, 0.7); +} + /* Тінь для книги */ .book-cover-container { box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.2); diff --git a/book-bazaar/src/app/components/book-details/book-details.html b/book-bazaar/src/app/components/book-details/book-details.html index 1b153d7..7242022 100644 --- a/book-bazaar/src/app/components/book-details/book-details.html +++ b/book-bazaar/src/app/components/book-details/book-details.html @@ -109,9 +109,56 @@
No similar books found.
+ } @else { +{{ book.author?.name }}
+Based on your reading history
+ } @else { +Trending now in our library
+ } +No recommendations available at the moment.
+ } @else { +{{ book.author?.name }}
+{{ book.price | currency }}
+Like, many, href=\"https://standardebooks.org/ebooks/thomas-hardy\">Hardy’s. This fiction novel uses Horror, Romance elements and themes from the description with clear author voice." }, { - "userId": "6aa97d44-776b-46e7-abe0-6451d04a7f45", + "userId": "non-fiction-user-2", "firstName": "Kai", "lastName": "Lopez", "avatarUrl": "https://i.pravatar.cc/150?u=6aa97d44-776b-46e7-abe0-6451d04a7f45", @@ -1920,7 +1920,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Fantasy, Horror. Not a perfect read but very worthwhile." }, { - "userId": "1c3e98bd-1a60-46b9-9c0d-448dd6f52c2f", + "userId": "mixed-reader-3", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=1c3e98bd-1a60-46b9-9c0d-448dd6f52c2f", @@ -1930,7 +1930,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "5a313e07-6b96-47c9-b141-81c74a295afb", + "userId": "mixed-reader-1", "firstName": "Rowan", "lastName": "Garcia", "avatarUrl": "https://i.pravatar.cc/150?u=5a313e07-6b96-47c9-b141-81c74a295afb", @@ -1940,7 +1940,7 @@ "text": "Henry James really shines in 'The Turn of the Screw' with governess, enigmatic, children. This non-fiction novel uses Fantasy, Horror elements and themes from the description with clear author voice." }, { - "userId": "99074c5d-d912-44b6-a75b-7da7f6438981", + "userId": "non-fiction-user-2", "firstName": "Jamie", "lastName": "Taylor", "avatarUrl": "https://i.pravatar.cc/150?u=99074c5d-d912-44b6-a75b-7da7f6438981", @@ -1950,7 +1950,7 @@ "text": "This book has good atmosphere, and the Horror, Fantasy influence is evident. It reminded me of the description's highlights." }, { - "userId": "55549c5b-28a9-4a77-966d-686e18c35746", + "userId": "non-fiction-user-4", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=55549c5b-28a9-4a77-966d-686e18c35746", @@ -1960,7 +1960,7 @@ "text": "The plot of 'Carrie' has strong Horror, Thriller moments; the characters are well-developed and the pacing is solid." }, { - "userId": "07a90399-0010-4bcd-bd46-2101bc750715", + "userId": "non-fiction-user-4", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=07a90399-0010-4bcd-bd46-2101bc750715", @@ -1970,7 +1970,7 @@ "text": "Stephen King really shines in 'Carrie' with story, misfit, high-school. This non-fiction novel uses Horror, Thriller elements and themes from the description with clear author voice." }, { - "userId": "ff01be00-6d24-4499-9bd3-8ce272eac542", + "userId": "mixed-reader-1", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=ff01be00-6d24-4499-9bd3-8ce272eac542", @@ -1980,7 +1980,7 @@ "text": "Good book, very nice." }, { - "userId": "d6690c70-1d1b-4fcd-8bd0-9e12dedc4766", + "userId": "non-fiction-user-1", "firstName": "Brooklyn", "lastName": "Young", "avatarUrl": "https://i.pravatar.cc/150?u=d6690c70-1d1b-4fcd-8bd0-9e12dedc4766", @@ -1990,7 +1990,7 @@ "text": "This book has good atmosphere, and the Horror, Thriller influence is evident. It reminded me of the description's highlights." }, { - "userId": "ed06bcc4-e27f-4995-bd71-681d14a8f84a", + "userId": "fiction-user-2", "firstName": "Reagan", "lastName": "Clark", "avatarUrl": "https://i.pravatar.cc/150?u=ed06bcc4-e27f-4995-bd71-681d14a8f84a", @@ -2000,7 +2000,7 @@ "text": "The plot of 'Wuthering Heights' has strong Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "0a87471d-e119-4137-bb42-69ca9f5c4a52", + "userId": "fiction-user-4", "firstName": "Kai", "lastName": "Lopez", "avatarUrl": null, @@ -2010,7 +2010,7 @@ "text": "Average book, use your judgment." }, { - "userId": "4673523f-a2e6-46cb-9fd5-5a40c410f519", + "userId": "mixed-reader-1", "firstName": "Devon", "lastName": "Hall", "avatarUrl": "https://i.pravatar.cc/150?u=4673523f-a2e6-46cb-9fd5-5a40c410f519", @@ -2020,7 +2020,7 @@ "text": "Emily Brontë really shines in 'Wuthering Heights' with Wuthering, Heights, 1847. This fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "9992decb-dee0-4811-94a0-0115be8dbce6", + "userId": "fiction-user-1", "firstName": "Baylor", "lastName": "Baker", "avatarUrl": "https://i.pravatar.cc/150?u=9992decb-dee0-4811-94a0-0115be8dbce6", @@ -2030,7 +2030,7 @@ "text": "Not my genre, but still pretty good." }, { - "userId": "65e1b7b7-3d9e-4517-92f2-8c8a439bb83b", + "userId": "non-fiction-user-2", "firstName": "Rowan", "lastName": "Green", "avatarUrl": "https://i.pravatar.cc/150?u=65e1b7b7-3d9e-4517-92f2-8c8a439bb83b", @@ -2040,7 +2040,7 @@ "text": "The plot of 'Emma' has strong Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "4d015de2-e915-4fdb-942e-aa088f7c690b", + "userId": "mixed-reader-3", "firstName": "Phoenix", "lastName": "Robinson", "avatarUrl": null, @@ -2050,7 +2050,7 @@ "text": "Jane Austen really shines in 'Emma' with Emma, Jane, Austen. This non-fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "6e7553f9-ddd3-42c3-b964-0cedadad9b4b", + "userId": "non-fiction-user-5", "firstName": "Dakota", "lastName": "Turner", "avatarUrl": "https://i.pravatar.cc/150?u=6e7553f9-ddd3-42c3-b964-0cedadad9b4b", @@ -2060,7 +2060,7 @@ "text": "Okay read, nothing special." }, { - "userId": "6d3c357a-f59d-4212-a2c9-d71f80caff27", + "userId": "mixed-reader-2", "firstName": "Kendall", "lastName": "Lewis", "avatarUrl": null, @@ -2070,7 +2070,7 @@ "text": "Good book, very nice." }, { - "userId": "7da6c8bc-6059-4e09-a534-eb0a2b90acaa", + "userId": "non-fiction-user-2", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=7da6c8bc-6059-4e09-a534-eb0a2b90acaa", @@ -2080,7 +2080,7 @@ "text": "Okay read, nothing special." }, { - "userId": "441d7ef0-d72e-4d10-b7e1-021a8fdb4185", + "userId": "non-fiction-user-4", "firstName": "Casey", "lastName": "Wilson", "avatarUrl": "https://i.pravatar.cc/150?u=441d7ef0-d72e-4d10-b7e1-021a8fdb4185", @@ -2090,7 +2090,7 @@ "text": "This book has good atmosphere, and the Romance, Classic influence is evident. It reminded me of the description's highlights." }, { - "userId": "a4c12dc5-2051-4821-8d4e-1b8fe0ffbe1c", + "userId": "non-fiction-user-5", "firstName": "Baylor", "lastName": "Baker", "avatarUrl": "https://i.pravatar.cc/150?u=a4c12dc5-2051-4821-8d4e-1b8fe0ffbe1c", @@ -2100,7 +2100,7 @@ "text": "Jane Austen really shines in 'Sense and Sensibility' with When, Dashwood, dies. This non-fiction novel uses Romance, Classic elements and themes from the description with clear author voice." }, { - "userId": "b6010c1e-aef6-4d93-9cb6-4efc631772a1", + "userId": "mixed-reader-1", "firstName": "Brooklyn", "lastName": "Young", "avatarUrl": "https://i.pravatar.cc/150?u=b6010c1e-aef6-4d93-9cb6-4efc631772a1", @@ -2110,7 +2110,7 @@ "text": "Average book, use your judgment." }, { - "userId": "84180932-99a3-4dbb-96a6-96252f165b60", + "userId": "mixed-reader-4", "firstName": "Devon", "lastName": "Hall", "avatarUrl": "https://i.pravatar.cc/150?u=84180932-99a3-4dbb-96a6-96252f165b60", @@ -2120,7 +2120,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "8ec3f542-126c-4725-a6ff-2ceaa2ab3f6f", + "userId": "non-fiction-user-4", "firstName": "Taylor", "lastName": "Brown", "avatarUrl": null, @@ -2130,7 +2130,7 @@ "text": "Okay read, nothing special." }, { - "userId": "73d3e44a-7474-4d4e-904b-f4093c7bbbc5", + "userId": "non-fiction-user-2", "firstName": "Taylor", "lastName": "Brown", "avatarUrl": "https://i.pravatar.cc/150?u=73d3e44a-7474-4d4e-904b-f4093c7bbbc5", @@ -2140,7 +2140,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Romance, Classic. Not a perfect read but very worthwhile." }, { - "userId": "bd02976b-976e-45bc-b84e-6de0f48da489", + "userId": "mixed-reader-2", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=bd02976b-976e-45bc-b84e-6de0f48da489", @@ -2150,7 +2150,7 @@ "text": "This book has good atmosphere, and the Romance, Classic influence is evident. It reminded me of the description's highlights." }, { - "userId": "ed8c1abb-af71-41a6-aa71-20369193478e", + "userId": "non-fiction-user-1", "firstName": "Dallas", "lastName": "Rodriguez", "avatarUrl": "https://i.pravatar.cc/150?u=ed8c1abb-af71-41a6-aa71-20369193478e", @@ -2160,7 +2160,7 @@ "text": "Louisa May Alcott really shines in 'Little Women' with Louisa, Alcotts, classic. This non-fiction novel uses Romance, Classic elements and themes from the description with clear author voice." }, { - "userId": "9a0b4d79-c3c1-430f-b2e3-53fc87df3d9a", + "userId": "biography-user-1", "firstName": "Riley", "lastName": "Moore", "avatarUrl": "https://i.pravatar.cc/150?u=9a0b4d79-c3c1-430f-b2e3-53fc87df3d9a", @@ -2170,7 +2170,7 @@ "text": "Not my genre, but still pretty good." }, { - "userId": "dd13f4c4-5dc6-4435-bae5-d45dd6cd2d7d", + "userId": "biography-user-3", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": null, @@ -2180,7 +2180,7 @@ "text": "I enjoyed the biography style, especially the parts that echo Romance. Not a perfect read but very worthwhile." }, { - "userId": "1ea0b9b2-7b5f-4dd5-b090-d700d8a70cdd", + "userId": "biography-user-5", "firstName": "Kendall", "lastName": "Lewis", "avatarUrl": "https://i.pravatar.cc/150?u=1ea0b9b2-7b5f-4dd5-b090-d700d8a70cdd", @@ -2190,7 +2190,7 @@ "text": "Лев Толстой really shines in 'Анна Каренина' with Described, William, Faulkner. This biography novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "374c1541-5c17-466d-8b5c-942a9adfd164", + "userId": "biography-user-3", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=374c1541-5c17-466d-8b5c-942a9adfd164", @@ -2200,7 +2200,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "0beb5ef0-2f68-4375-b5b4-dd1e2aee1b32", + "userId": "biography-user-2", "firstName": "Devon", "lastName": "Hall", "avatarUrl": "https://i.pravatar.cc/150?u=0beb5ef0-2f68-4375-b5b4-dd1e2aee1b32", @@ -2210,7 +2210,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "9e86125d-3e23-41e5-82e8-b07972f7d296", + "userId": "non-fiction-user-4", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=9e86125d-3e23-41e5-82e8-b07972f7d296", @@ -2220,7 +2220,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Romance. Not a perfect read but very worthwhile." }, { - "userId": "1f06a526-1d67-45d9-8f83-e80e4c44ba17", + "userId": "non-fiction-user-3", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": null, @@ -2230,7 +2230,7 @@ "text": "Good book, very nice." }, { - "userId": "8fac95a1-c238-4c23-b30e-98ed8bf0312a", + "userId": "non-fiction-user-1", "firstName": "Aiden", "lastName": "Allen", "avatarUrl": "https://i.pravatar.cc/150?u=8fac95a1-c238-4c23-b30e-98ed8bf0312a", @@ -2240,7 +2240,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "048f556e-7d46-4e54-8b4c-07c669e7a156", + "userId": "non-fiction-user-5", "firstName": "Marley", "lastName": "Nelson", "avatarUrl": "https://i.pravatar.cc/150?u=048f556e-7d46-4e54-8b4c-07c669e7a156", @@ -2250,7 +2250,7 @@ "text": "Charlotte Brontë really shines in 'Jane Eyre' with novel, somewhere, north. This non-fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "95793ff5-0a9d-48b1-9e49-d752e0cfdfc2", + "userId": "non-fiction-user-3", "firstName": "Avery", "lastName": "Thompson", "avatarUrl": "https://i.pravatar.cc/150?u=95793ff5-0a9d-48b1-9e49-d752e0cfdfc2", @@ -2260,7 +2260,7 @@ "text": "Jane Austen really shines in 'Northanger Abbey' with Northanger, Abbey, both. This non-fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "638282ae-9f78-4f88-a72e-b93109b6eef8", + "userId": "mixed-reader-1", "firstName": "Shawn", "lastName": "Adams", "avatarUrl": null, @@ -2270,7 +2270,7 @@ "text": "Average book, use your judgment." }, { - "userId": "3a9e0ea5-4e65-491f-97a9-9f88c7f44baf", + "userId": "non-fiction-user-3", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=3a9e0ea5-4e65-491f-97a9-9f88c7f44baf", @@ -2280,7 +2280,7 @@ "text": "This book has good atmosphere, and the Romance influence is evident. It reminded me of the description's highlights." }, { - "userId": "894888d4-146c-4bb8-b142-353a97dd1e69", + "userId": "non-fiction-user-5", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": "https://i.pravatar.cc/150?u=894888d4-146c-4bb8-b142-353a97dd1e69", @@ -2290,7 +2290,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "115c3f7a-a2a3-4e90-b170-1a4ae6be63c2", + "userId": "fiction-user-2", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=115c3f7a-a2a3-4e90-b170-1a4ae6be63c2", @@ -2300,7 +2300,7 @@ "text": "Okay read, nothing special." }, { - "userId": "049bfaec-f3c7-4631-9329-0e342cb4b07f", + "userId": "fiction-user-3", "firstName": "Riley", "lastName": "Moore", "avatarUrl": null, @@ -2310,7 +2310,7 @@ "text": "This book has good atmosphere, and the Romance influence is evident. It reminded me of the description's highlights." }, { - "userId": "91e12c6b-6747-422f-8a4b-5d1148b5f7d8", + "userId": "mixed-reader-5", "firstName": "Rowan", "lastName": "Green", "avatarUrl": "https://i.pravatar.cc/150?u=91e12c6b-6747-422f-8a4b-5d1148b5f7d8", @@ -2320,7 +2320,7 @@ "text": "This book has good atmosphere, and the Romance influence is evident. It reminded me of the description's highlights." }, { - "userId": "4f20e719-f42d-4ccb-94b8-666b5129b836", + "userId": "fiction-user-3", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": "https://i.pravatar.cc/150?u=4f20e719-f42d-4ccb-94b8-666b5129b836", @@ -2330,7 +2330,7 @@ "text": "Edith Wharton really shines in 'Ethan Frome' with *Edith, Wharton, wrote. This fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "90f9fbe8-b36f-4d7f-adbd-09fd098d8159", + "userId": "fiction-user-2", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=90f9fbe8-b36f-4d7f-adbd-09fd098d8159", @@ -2340,7 +2340,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "602477ee-5c7b-4d65-a933-05f6acf191e7", + "userId": "non-fiction-user-4", "firstName": "Riley", "lastName": "Moore", "avatarUrl": null, @@ -2350,7 +2350,7 @@ "text": "Alexandre Dumas really shines in 'Le Comte de Monte Cristo' with Thrown, prison, crime. This non-fiction novel uses Romance, Classic elements and themes from the description with clear author voice." }, { - "userId": "82463d46-2c47-45f1-94aa-39ee42e52efa", + "userId": "non-fiction-user-5", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": "https://i.pravatar.cc/150?u=82463d46-2c47-45f1-94aa-39ee42e52efa", @@ -2360,7 +2360,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "d24343b8-103d-429c-8695-c446c9da9637", + "userId": "mixed-reader-4", "firstName": "Baylor", "lastName": "Baker", "avatarUrl": "https://i.pravatar.cc/150?u=d24343b8-103d-429c-8695-c446c9da9637", @@ -2370,7 +2370,7 @@ "text": "Okay read, nothing special." }, { - "userId": "e5a7e3f1-b5f7-4599-9d49-2bedf4c6356a", + "userId": "mixed-reader-4", "firstName": "Quinn", "lastName": "White", "avatarUrl": "https://i.pravatar.cc/150?u=e5a7e3f1-b5f7-4599-9d49-2bedf4c6356a", @@ -2380,7 +2380,7 @@ "text": "The plot of 'Le Comte de Monte Cristo' has strong Classic, Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "893615a1-8b25-42d9-a2ba-92bbb926b3e1", + "userId": "non-fiction-user-4", "firstName": "Kendall", "lastName": "Lewis", "avatarUrl": "https://i.pravatar.cc/150?u=893615a1-8b25-42d9-a2ba-92bbb926b3e1", @@ -2390,7 +2390,7 @@ "text": "Zane Grey really shines in 'Riders of the Purple Sage' with Riders, Purple, Sage. This non-fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "b4c7d617-37c5-4101-bfd0-a090b0a830d2", + "userId": "non-fiction-user-4", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": null, @@ -2400,7 +2400,7 @@ "text": "The plot of 'Riders of the Purple Sage' has strong Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "3b532221-7395-47fa-8ad1-e42cecb3efe8", + "userId": "non-fiction-user-5", "firstName": "Finley", "lastName": "Wright", "avatarUrl": "https://i.pravatar.cc/150?u=3b532221-7395-47fa-8ad1-e42cecb3efe8", @@ -2410,7 +2410,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "78f2d3b1-2d2b-439d-a6f2-e70a6474dc33", + "userId": "non-fiction-user-3", "firstName": "Hayden", "lastName": "Jackson", "avatarUrl": "https://i.pravatar.cc/150?u=78f2d3b1-2d2b-439d-a6f2-e70a6474dc33", @@ -2420,7 +2420,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "95679f9b-4a2c-44bc-9e4e-b5525053d840", + "userId": "mixed-reader-3", "firstName": "Brooklyn", "lastName": "Young", "avatarUrl": "https://i.pravatar.cc/150?u=95679f9b-4a2c-44bc-9e4e-b5525053d840", @@ -2430,7 +2430,7 @@ "text": "The plot of 'A Room with a View' has strong Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "5638628f-2a36-49b4-965c-8cadac6f9bdc", + "userId": "fiction-user-5", "firstName": "Quinn", "lastName": "White", "avatarUrl": "https://i.pravatar.cc/150?u=5638628f-2a36-49b4-965c-8cadac6f9bdc", @@ -2440,7 +2440,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "4632d438-a775-4536-abc1-8bfcc31ffbeb", + "userId": "fiction-user-5", "firstName": "Reese", "lastName": "Martin", "avatarUrl": "https://i.pravatar.cc/150?u=4632d438-a775-4536-abc1-8bfcc31ffbeb", @@ -2450,7 +2450,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "529e9f3a-4c78-48c1-9117-211e262668ea", + "userId": "fiction-user-4", "firstName": "Shawn", "lastName": "Adams", "avatarUrl": "https://i.pravatar.cc/150?u=529e9f3a-4c78-48c1-9117-211e262668ea", @@ -2460,7 +2460,7 @@ "text": "E. M. Forster really shines in 'A Room with a View' with Lucy, rigid, middle-class. This fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "a620b456-4aa9-42ac-bd7e-d3ea6e284303", + "userId": "fiction-user-5", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=a620b456-4aa9-42ac-bd7e-d3ea6e284303", @@ -2470,7 +2470,7 @@ "text": "I enjoyed the fiction style, especially the parts that echo Romance. Not a perfect read but very worthwhile." }, { - "userId": "0330a18e-1695-4f72-a0dd-6a07ff7fea19", + "userId": "mixed-reader-5", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=0330a18e-1695-4f72-a0dd-6a07ff7fea19", @@ -2480,7 +2480,7 @@ "text": "Harriet Beecher Stowe really shines in 'Uncle Tom's Cabin' with This, unforgettable, novel. This non-fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "9bbf50c6-dd0c-4ebc-9204-34928cbfaf3b", + "userId": "non-fiction-user-1", "firstName": "Jordan", "lastName": "Mitchell", "avatarUrl": null, @@ -2490,7 +2490,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "1f982af8-cbb2-4e4f-9542-d2730a88f3ac", + "userId": "mixed-reader-1", "firstName": "Brooklyn", "lastName": "Young", "avatarUrl": "https://i.pravatar.cc/150?u=1f982af8-cbb2-4e4f-9542-d2730a88f3ac", @@ -2500,7 +2500,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "09974338-6408-4be0-8686-2a88e426e513", + "userId": "non-fiction-user-2", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=09974338-6408-4be0-8686-2a88e426e513", @@ -2510,7 +2510,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "94cae71c-fcf8-4200-a9f0-885fbcb1bd92", + "userId": "non-fiction-user-2", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=94cae71c-fcf8-4200-a9f0-885fbcb1bd92", @@ -2520,7 +2520,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Romance. Not a perfect read but very worthwhile." }, { - "userId": "33f7a5e0-582b-47f3-8779-09d71a581fea", + "userId": "fiction-user-5", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": "https://i.pravatar.cc/150?u=33f7a5e0-582b-47f3-8779-09d71a581fea", @@ -2530,7 +2530,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "d20e9efc-1918-45bc-9b28-a9648e5bde65", + "userId": "fiction-user-4", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=d20e9efc-1918-45bc-9b28-a9648e5bde65", @@ -2540,7 +2540,7 @@ "text": "D. H. Lawrence really shines in 'Women in Love' with Dark, filled, with. This fiction novel uses Fantasy, Romance elements and themes from the description with clear author voice." }, { - "userId": "dc14ce34-ad5c-4609-9ba4-9e11e5375007", + "userId": "fiction-user-5", "firstName": "Baylor", "lastName": "Baker", "avatarUrl": "https://i.pravatar.cc/150?u=dc14ce34-ad5c-4609-9ba4-9e11e5375007", @@ -2550,7 +2550,7 @@ "text": "Okay read, nothing special." }, { - "userId": "ad63290c-4c60-4c78-abf3-26649d957f18", + "userId": "mixed-reader-3", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": null, @@ -2560,7 +2560,7 @@ "text": "The plot of 'Women in Love' has strong Romance, Fantasy moments; the characters are well-developed and the pacing is solid." }, { - "userId": "fe2c84a8-bc1b-455f-a199-58572213f4d7", + "userId": "non-fiction-user-4", "firstName": "Hayden", "lastName": "Jackson", "avatarUrl": "https://i.pravatar.cc/150?u=fe2c84a8-bc1b-455f-a199-58572213f4d7", @@ -2570,7 +2570,7 @@ "text": "Joseph Conrad really shines in 'Heart of Darkness' with Heart, Darkness, (1899). This non-fiction novel uses Romance, Mystery elements and themes from the description with clear author voice." }, { - "userId": "6d3110dc-bc83-4c15-b9b5-3bc2c5030220", + "userId": "non-fiction-user-1", "firstName": "Cameron", "lastName": "Anderson", "avatarUrl": null, @@ -2580,7 +2580,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "ea50b366-9131-4074-8719-84857de596b6", + "userId": "non-fiction-user-5", "firstName": "Hayden", "lastName": "Jackson", "avatarUrl": "https://i.pravatar.cc/150?u=ea50b366-9131-4074-8719-84857de596b6", @@ -2590,7 +2590,7 @@ "text": "This book has good atmosphere, and the Mystery, Romance influence is evident. It reminded me of the description's highlights." }, { - "userId": "e31e23b0-b0bc-4ce7-a540-94ab64e49dad", + "userId": "non-fiction-user-2", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=e31e23b0-b0bc-4ce7-a540-94ab64e49dad", @@ -2600,7 +2600,7 @@ "text": "This book has good atmosphere, and the Mystery, Romance influence is evident. It reminded me of the description's highlights." }, { - "userId": "46d28898-de40-45f0-a83c-452559ee67dc", + "userId": "fiction-user-2", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=46d28898-de40-45f0-a83c-452559ee67dc", @@ -2610,7 +2610,7 @@ "text": "E. M. Forster really shines in 'Howards End' with Howards, novel, Forster. This fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "6471c574-e701-4b56-9405-51befecdaaf3", + "userId": "mixed-reader-1", "firstName": "Kendall", "lastName": "Lewis", "avatarUrl": null, @@ -2620,7 +2620,7 @@ "text": "This book has good atmosphere, and the Romance influence is evident. It reminded me of the description's highlights." }, { - "userId": "3d588710-13bf-4ff0-826f-bea5f33137cd", + "userId": "fiction-user-5", "firstName": "Avery", "lastName": "Thompson", "avatarUrl": "https://i.pravatar.cc/150?u=3d588710-13bf-4ff0-826f-bea5f33137cd", @@ -2630,7 +2630,7 @@ "text": "Average book, use your judgment." }, { - "userId": "085b051c-9694-4ebc-bae2-7ff19b47f81c", + "userId": "fiction-user-4", "firstName": "Devon", "lastName": "Hall", "avatarUrl": "https://i.pravatar.cc/150?u=085b051c-9694-4ebc-bae2-7ff19b47f81c", @@ -2640,7 +2640,7 @@ "text": "The plot of 'Howards End' has strong Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "1705f07e-6919-450e-9a09-7a2d9bf1ea62", + "userId": "fiction-user-1", "firstName": "Jordan", "lastName": "Mitchell", "avatarUrl": "https://i.pravatar.cc/150?u=1705f07e-6919-450e-9a09-7a2d9bf1ea62", @@ -2650,7 +2650,7 @@ "text": "Henry James really shines in 'The Portrait of a Lady' with Young, American, Isabel. This fiction novel uses Romance elements and themes from the description with clear author voice." }, { - "userId": "dee7af8a-6f98-48f6-8c55-1e2714a9bda3", + "userId": "fiction-user-5", "firstName": "Remy", "lastName": "Roberts", "avatarUrl": null, @@ -2660,7 +2660,7 @@ "text": "Not my genre, but still pretty good." }, { - "userId": "7a1da435-7a4c-4db3-b9c4-42f07150c0ab", + "userId": "fiction-user-2", "firstName": "Marley", "lastName": "Nelson", "avatarUrl": null, @@ -2670,7 +2670,7 @@ "text": "I enjoyed the fiction style, especially the parts that echo Romance. Not a perfect read but very worthwhile." }, { - "userId": "5f25b317-3396-4db8-ba1c-19442817e207", + "userId": "fiction-user-1", "firstName": "Finley", "lastName": "Wright", "avatarUrl": "https://i.pravatar.cc/150?u=5f25b317-3396-4db8-ba1c-19442817e207", @@ -2680,7 +2680,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "6632a68e-419a-46c3-86db-607daf4cdb1d", + "userId": "fiction-user-1", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": null, @@ -2690,7 +2690,7 @@ "text": "The plot of 'The Portrait of a Lady' has strong Romance moments; the characters are well-developed and the pacing is solid." }, { - "userId": "f9c38fac-084c-445c-8aa7-54be10c0f04d", + "userId": "non-fiction-user-4", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=f9c38fac-084c-445c-8aa7-54be10c0f04d", @@ -2700,7 +2700,7 @@ "text": "Average book, use your judgment." }, { - "userId": "182ecc80-0a38-47bc-adb1-8646f61bb148", + "userId": "non-fiction-user-2", "firstName": "Dallas", "lastName": "Rodriguez", "avatarUrl": "https://i.pravatar.cc/150?u=182ecc80-0a38-47bc-adb1-8646f61bb148", @@ -2710,7 +2710,7 @@ "text": "This book has good atmosphere, and the Fantasy influence is evident. It reminded me of the description's highlights." }, { - "userId": "66a799ce-1bb1-4924-8ddc-4c9bd7f08293", + "userId": "non-fiction-user-1", "firstName": "Dakota", "lastName": "Turner", "avatarUrl": null, @@ -2720,7 +2720,7 @@ "text": "Charles Dickens really shines in 'A Christmas Carol' with retelling, story, about. This non-fiction novel uses Fantasy elements and themes from the description with clear author voice." }, { - "userId": "72cd369b-a66b-4756-bace-7c722af8f006", + "userId": "mixed-reader-2", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=72cd369b-a66b-4756-bace-7c722af8f006", @@ -2730,7 +2730,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "94fe201f-4d4c-4b8b-a81e-8cbb4c574e62", + "userId": "non-fiction-user-5", "firstName": "Reagan", "lastName": "Clark", "avatarUrl": null, @@ -2740,7 +2740,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Fantasy. Not a perfect read but very worthwhile." }, { - "userId": "4452917f-5054-476d-87bf-515410660cfc", + "userId": "mixed-reader-4", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": "https://i.pravatar.cc/150?u=4452917f-5054-476d-87bf-515410660cfc", @@ -2750,7 +2750,7 @@ "text": "The plot of 'Adventures of Huckleberry Finn' has strong Classic, Adventure moments; the characters are well-developed and the pacing is solid." }, { - "userId": "211c57a1-0ad2-4fb4-ad10-48c82fdc925f", + "userId": "non-fiction-user-1", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=211c57a1-0ad2-4fb4-ad10-48c82fdc925f", @@ -2760,7 +2760,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "f9996357-fc84-436d-a4fd-875148a3a530", + "userId": "non-fiction-user-2", "firstName": "Remy", "lastName": "Roberts", "avatarUrl": "https://i.pravatar.cc/150?u=f9996357-fc84-436d-a4fd-875148a3a530", @@ -2770,7 +2770,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "81affbce-15cc-41cc-8c2b-43f3a0fac1b0", + "userId": "non-fiction-user-3", "firstName": "Aiden", "lastName": "Allen", "avatarUrl": "https://i.pravatar.cc/150?u=81affbce-15cc-41cc-8c2b-43f3a0fac1b0", @@ -2780,7 +2780,7 @@ "text": "This book has good atmosphere, and the Adventure, Classic influence is evident. It reminded me of the description's highlights." }, { - "userId": "06dc93a4-f971-4296-b115-8393930563b9", + "userId": "mixed-reader-2", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=06dc93a4-f971-4296-b115-8393930563b9", @@ -2790,7 +2790,7 @@ "text": "Mark Twain really shines in 'Adventures of Huckleberry Finn' with Adventures, Huckleberry, Finn. This non-fiction novel uses Adventure, Classic elements and themes from the description with clear author voice." }, { - "userId": "beb9dbda-aa6a-459e-b52f-c5e4d45fc57e", + "userId": "mixed-reader-1", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=beb9dbda-aa6a-459e-b52f-c5e4d45fc57e", @@ -2800,7 +2800,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Adventure. Not a perfect read but very worthwhile." }, { - "userId": "1301f2eb-fc96-4528-b371-1c476f3d1c8a", + "userId": "non-fiction-user-1", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=1301f2eb-fc96-4528-b371-1c476f3d1c8a", @@ -2810,7 +2810,7 @@ "text": "Daniel Defoe really shines in 'Robinson Crusoe' with During, several, adventurous. This non-fiction novel uses Adventure elements and themes from the description with clear author voice." }, { - "userId": "481a4166-48bb-4712-93c8-fd919246b503", + "userId": "non-fiction-user-3", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": null, @@ -2820,7 +2820,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "44e00927-4a28-4b3c-8299-bdd5ee137e4f", + "userId": "mixed-reader-3", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=44e00927-4a28-4b3c-8299-bdd5ee137e4f", @@ -2830,7 +2830,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "99f64c5e-96d7-491e-b604-18ba4250438a", + "userId": "mixed-reader-4", "firstName": "Marley", "lastName": "Nelson", "avatarUrl": "https://i.pravatar.cc/150?u=99f64c5e-96d7-491e-b604-18ba4250438a", @@ -2840,7 +2840,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "db54facd-70cb-47a8-ba6a-d53319cc619d", + "userId": "mixed-reader-1", "firstName": "Alex", "lastName": "Smith", "avatarUrl": "https://i.pravatar.cc/150?u=db54facd-70cb-47a8-ba6a-d53319cc619d", @@ -2850,7 +2850,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "f866943c-2bc2-4f88-bc33-c8b7a1a155b5", + "userId": "mixed-reader-1", "firstName": "Kai", "lastName": "Lopez", "avatarUrl": "https://i.pravatar.cc/150?u=f866943c-2bc2-4f88-bc33-c8b7a1a155b5", @@ -2860,7 +2860,7 @@ "text": "Average book, use your judgment." }, { - "userId": "d5a776f9-9f17-47c8-be3e-9493d4593ca2", + "userId": "non-fiction-user-2", "firstName": "Casey", "lastName": "Wilson", "avatarUrl": "https://i.pravatar.cc/150?u=d5a776f9-9f17-47c8-be3e-9493d4593ca2", @@ -2870,7 +2870,7 @@ "text": "The plot of 'Hamlet' has strong Classic moments; the characters are well-developed and the pacing is solid." }, { - "userId": "a1892ef5-0918-447f-afcc-748ee47562d1", + "userId": "mixed-reader-3", "firstName": "Finley", "lastName": "Wright", "avatarUrl": null, @@ -2880,7 +2880,7 @@ "text": "William Shakespeare really shines in 'Hamlet' with this, quintessential, Shakespeare. This non-fiction novel uses Classic elements and themes from the description with clear author voice." }, { - "userId": "3e3e4562-7b4c-4916-bf9b-94e8eea85661", + "userId": "non-fiction-user-5", "firstName": "Finley", "lastName": "Wright", "avatarUrl": "https://i.pravatar.cc/150?u=3e3e4562-7b4c-4916-bf9b-94e8eea85661", @@ -2890,7 +2890,7 @@ "text": "Okay read, nothing special." }, { - "userId": "096e5c52-a971-4ccf-9407-8be1378def42", + "userId": "mixed-reader-1", "firstName": "Alex", "lastName": "Smith", "avatarUrl": "https://i.pravatar.cc/150?u=096e5c52-a971-4ccf-9407-8be1378def42", @@ -2900,7 +2900,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "cf5cf270-2e1c-4ff0-94c9-d661ead44054", + "userId": "non-fiction-user-1", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=cf5cf270-2e1c-4ff0-94c9-d661ead44054", @@ -2910,7 +2910,7 @@ "text": "William Shakespeare really shines in 'Macbeth' with play, concerns, trusted. This non-fiction novel uses elements and themes from the description with clear author voice." }, { - "userId": "6c22a6ca-0e0a-4860-8751-3623f5840f19", + "userId": "non-fiction-user-4", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=6c22a6ca-0e0a-4860-8751-3623f5840f19", @@ -2920,7 +2920,7 @@ "text": "This book has good atmosphere, and the Non-Fiction influence is evident. It reminded me of the description's highlights." }, { - "userId": "dbcf3dca-a971-4e6f-bf7d-690420c1bf39", + "userId": "non-fiction-user-1", "firstName": "Cameron", "lastName": "Anderson", "avatarUrl": null, @@ -2930,7 +2930,7 @@ "text": "The plot of 'Macbeth' has strong Non-Fiction moments; the characters are well-developed and the pacing is solid." }, { - "userId": "2805009e-cd2b-444c-aca2-204e376c5b22", + "userId": "mixed-reader-1", "firstName": "Jordan", "lastName": "Lee", "avatarUrl": null, @@ -2940,7 +2940,7 @@ "text": "Average book, use your judgment." }, { - "userId": "4bcbc93d-d121-43df-a6cf-0650de737c5d", + "userId": "mixed-reader-2", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=4bcbc93d-d121-43df-a6cf-0650de737c5d", @@ -2950,7 +2950,7 @@ "text": "Good book, very nice." }, { - "userId": "308650ee-5007-4fb5-b965-bb4efbe50482", + "userId": "mixed-reader-3", "firstName": "Kendall", "lastName": "Lewis", "avatarUrl": "https://i.pravatar.cc/150?u=308650ee-5007-4fb5-b965-bb4efbe50482", @@ -2960,7 +2960,7 @@ "text": "This book has good atmosphere, and the Science Fiction, Thriller influence is evident. It reminded me of the description's highlights." }, { - "userId": "e34a4211-0e15-4c3b-8f9e-02ea9d635951", + "userId": "technology-user-2", "firstName": "Finley", "lastName": "Wright", "avatarUrl": "https://i.pravatar.cc/150?u=e34a4211-0e15-4c3b-8f9e-02ea9d635951", @@ -2970,7 +2970,7 @@ "text": "Isaac Asimov really shines in 'Les Robots' with Robot, fixup, novel. This technology novel uses Science Fiction, Thriller elements and themes from the description with clear author voice." }, { - "userId": "31ceb969-debe-48a3-9968-f5dddb022f5f", + "userId": "technology-user-3", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=31ceb969-debe-48a3-9968-f5dddb022f5f", @@ -2980,7 +2980,7 @@ "text": "The plot of 'Les Robots' has strong Thriller, Science Fiction moments; the characters are well-developed and the pacing is solid." }, { - "userId": "cff57be9-9734-44bc-8980-4bf45ac54ddf", + "userId": "technology-user-2", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=cff57be9-9734-44bc-8980-4bf45ac54ddf", @@ -2990,7 +2990,7 @@ "text": "Eoin Colfer really shines in 'Artemis Fowl' with Artemis, Fowl, first. This technology novel uses Fantasy elements and themes from the description with clear author voice." }, { - "userId": "849b7a32-9456-48d4-8bce-5f06e6fae02a", + "userId": "technology-user-3", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=849b7a32-9456-48d4-8bce-5f06e6fae02a", @@ -3000,7 +3000,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "5fe68926-9a2b-46b8-87f0-e885de5f0107", + "userId": "mixed-reader-3", "firstName": "Jordan", "lastName": "Mitchell", "avatarUrl": "https://i.pravatar.cc/150?u=5fe68926-9a2b-46b8-87f0-e885de5f0107", @@ -3010,7 +3010,7 @@ "text": "I enjoyed the technology style, especially the parts that echo Fantasy. Not a perfect read but very worthwhile." }, { - "userId": "d523620a-5ff4-4934-9da4-93882e894c3b", + "userId": "technology-user-3", "firstName": "Avery", "lastName": "Thompson", "avatarUrl": "https://i.pravatar.cc/150?u=d523620a-5ff4-4934-9da4-93882e894c3b", @@ -3020,7 +3020,7 @@ "text": "This book has good atmosphere, and the Fantasy influence is evident. It reminded me of the description's highlights." }, { - "userId": "4f42c413-7e79-48e8-b84c-b305faa87e37", + "userId": "technology-user-1", "firstName": "Blake", "lastName": "Walker", "avatarUrl": "https://i.pravatar.cc/150?u=4f42c413-7e79-48e8-b84c-b305faa87e37", @@ -3030,7 +3030,7 @@ "text": "Good book, very nice." }, { - "userId": "18170994-3791-41d7-bac7-183ff9d85d12", + "userId": "mixed-reader-5", "firstName": "Reese", "lastName": "Martin", "avatarUrl": "https://i.pravatar.cc/150?u=18170994-3791-41d7-bac7-183ff9d85d12", @@ -3040,7 +3040,7 @@ "text": "John R. Levine really shines in 'The Internet for Dummies' with Covers, hardware, software. This technology novel uses elements and themes from the description with clear author voice." }, { - "userId": "efd481c1-8b0e-4e2b-ad6b-793ed5984d47", + "userId": "technology-user-5", "firstName": "Rowan", "lastName": "Green", "avatarUrl": "https://i.pravatar.cc/150?u=efd481c1-8b0e-4e2b-ad6b-793ed5984d47", @@ -3050,7 +3050,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "51118890-3071-4078-a007-e6a6f497f7e2", + "userId": "technology-user-3", "firstName": "Avery", "lastName": "Thompson", "avatarUrl": "https://i.pravatar.cc/150?u=51118890-3071-4078-a007-e6a6f497f7e2", @@ -3060,7 +3060,7 @@ "text": "This book has good atmosphere, and the Technology influence is evident. It reminded me of the description's highlights." }, { - "userId": "ff6ff9da-43ee-4ab5-a507-ca9ea0961503", + "userId": "technology-user-4", "firstName": "Dallas", "lastName": "Rodriguez", "avatarUrl": "https://i.pravatar.cc/150?u=ff6ff9da-43ee-4ab5-a507-ca9ea0961503", @@ -3070,7 +3070,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "809913a9-7275-42fd-b9a0-cbfc4adbaf9f", + "userId": "technology-user-2", "firstName": "Dallas", "lastName": "Rodriguez", "avatarUrl": null, @@ -3080,7 +3080,7 @@ "text": "Not my genre, but still pretty good." }, { - "userId": "1a3a98e0-81cd-4ab4-beb1-3155e343dea5", + "userId": "technology-user-4", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=1a3a98e0-81cd-4ab4-beb1-3155e343dea5", @@ -3090,7 +3090,7 @@ "text": "The plot of 'Stormbreaker' has strong Thriller, Mystery moments; the characters are well-developed and the pacing is solid." }, { - "userId": "7308f7f3-4cbb-4384-983d-c6fb5bf62ef4", + "userId": "technology-user-2", "firstName": "Blake", "lastName": "Walker", "avatarUrl": null, @@ -3100,7 +3100,7 @@ "text": "Good book, very nice." }, { - "userId": "caa26465-9214-460e-b56c-40b56ed98b9b", + "userId": "technology-user-4", "firstName": "Shawn", "lastName": "Adams", "avatarUrl": "https://i.pravatar.cc/150?u=caa26465-9214-460e-b56c-40b56ed98b9b", @@ -3110,7 +3110,7 @@ "text": "Anthony Horowitz really shines in 'Stormbreaker' with They, told, uncle. This technology novel uses Mystery, Thriller elements and themes from the description with clear author voice." }, { - "userId": "3fcf782f-ce82-4f68-8fa1-6c3a2b2a98d1", + "userId": "technology-user-5", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=3fcf782f-ce82-4f68-8fa1-6c3a2b2a98d1", @@ -3120,7 +3120,7 @@ "text": "I enjoyed the technology style, especially the parts that echo Thriller, Mystery. Not a perfect read but very worthwhile." }, { - "userId": "976d35ba-cb10-4719-a821-95c51518e601", + "userId": "mixed-reader-2", "firstName": "Alex", "lastName": "Smith", "avatarUrl": null, @@ -3130,7 +3130,7 @@ "text": "This book has good atmosphere, and the Technology influence is evident. It reminded me of the description's highlights." }, { - "userId": "addd2825-6bfb-4dc1-901d-26b547d4a930", + "userId": "technology-user-5", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=addd2825-6bfb-4dc1-901d-26b547d4a930", @@ -3140,7 +3140,7 @@ "text": "Okay read, nothing special." }, { - "userId": "03a7d89a-e25d-4db4-9557-b9e82b1bc4a8", + "userId": "technology-user-4", "firstName": "Shawn", "lastName": "Adams", "avatarUrl": null, @@ -3150,7 +3150,7 @@ "text": "Good book, very nice." }, { - "userId": "ae615a71-c7b2-49c5-be59-03a90a39458d", + "userId": "technology-user-4", "firstName": "Alex", "lastName": "Smith", "avatarUrl": null, @@ -3160,7 +3160,7 @@ "text": "Scott Mueller really shines in 'Upgrading and repairing PCs' with Runaway, best-selling, hardware. This technology novel uses elements and themes from the description with clear author voice." }, { - "userId": "d901492f-59d1-461a-9ab5-17ab944aea06", + "userId": "technology-user-1", "firstName": "Drew", "lastName": "King", "avatarUrl": null, @@ -3170,7 +3170,7 @@ "text": "Stuart J. Russell really shines in 'Artificial intelligence' with comprehensive, up-to-date, introduction. This technology novel uses elements and themes from the description with clear author voice." }, { - "userId": "0db6c5e9-16a8-45ba-85e7-550e11da3798", + "userId": "technology-user-4", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=0db6c5e9-16a8-45ba-85e7-550e11da3798", @@ -3180,7 +3180,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "7db3ea89-c64b-4057-85b1-d349b8adca2b", + "userId": "technology-user-1", "firstName": "Riley", "lastName": "Moore", "avatarUrl": "https://i.pravatar.cc/150?u=7db3ea89-c64b-4057-85b1-d349b8adca2b", @@ -3190,7 +3190,7 @@ "text": "The plot of 'Artificial intelligence' has strong Technology moments; the characters are well-developed and the pacing is solid." }, { - "userId": "ea6cc920-57a6-4528-a391-77dc11ee0a62", + "userId": "technology-user-1", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=ea6cc920-57a6-4528-a391-77dc11ee0a62", @@ -3200,7 +3200,7 @@ "text": "The plot of 'Artificial intelligence' has strong Technology moments; the characters are well-developed and the pacing is solid." }, { - "userId": "2fd22de2-2f10-4e02-9f9d-5d7be41b166f", + "userId": "mixed-reader-3", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=2fd22de2-2f10-4e02-9f9d-5d7be41b166f", @@ -3210,7 +3210,7 @@ "text": "The plot of 'Computer Concepts' has strong Technology moments; the characters are well-developed and the pacing is solid." }, { - "userId": "923cc45e-030c-497c-aebe-7d6947eaa6f1", + "userId": "technology-user-2", "firstName": "Reagan", "lastName": "Clark", "avatarUrl": "https://i.pravatar.cc/150?u=923cc45e-030c-497c-aebe-7d6947eaa6f1", @@ -3220,7 +3220,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "347caad8-a7bb-4205-a26c-1171aeb5c454", + "userId": "technology-user-2", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=347caad8-a7bb-4205-a26c-1171aeb5c454", @@ -3230,7 +3230,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "3023edda-bac6-480c-aa1f-744583751b9f", + "userId": "technology-user-2", "firstName": "Remy", "lastName": "Roberts", "avatarUrl": null, @@ -3240,7 +3240,7 @@ "text": "June Jamrich Parsons really shines in 'Computer Concepts' with description, available. This technology novel uses elements and themes from the description with clear author voice." }, { - "userId": "45da94f9-1b7f-427f-8973-ccacf4a8623d", + "userId": "technology-user-2", "firstName": "Morgan", "lastName": "Davis", "avatarUrl": null, @@ -3250,7 +3250,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "dc34996f-bcf7-4276-9d0a-c71153e6e212", + "userId": "technology-user-4", "firstName": "Dakota", "lastName": "Turner", "avatarUrl": "https://i.pravatar.cc/150?u=dc34996f-bcf7-4276-9d0a-c71153e6e212", @@ -3260,7 +3260,7 @@ "text": "This book has good atmosphere, and the Science Fiction influence is evident. It reminded me of the description's highlights." }, { - "userId": "727fb121-68d5-4e01-8721-08e3fc1273ee", + "userId": "mixed-reader-1", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": null, @@ -3270,7 +3270,7 @@ "text": "This book has good atmosphere, and the Science Fiction influence is evident. It reminded me of the description's highlights." }, { - "userId": "eaed5610-eb41-4685-8c6c-1571cf1b2da6", + "userId": "mixed-reader-2", "firstName": "Rowan", "lastName": "Garcia", "avatarUrl": "https://i.pravatar.cc/150?u=eaed5610-eb41-4685-8c6c-1571cf1b2da6", @@ -3280,7 +3280,7 @@ "text": "William Gibson really shines in 'Mona Lisa Overdrive' with Mona, Lisa, Overdrive. This technology novel uses Science Fiction elements and themes from the description with clear author voice." }, { - "userId": "2f150349-bfd7-4dc0-bfa2-6030bcb0cc71", + "userId": "technology-user-4", "firstName": "Jamie", "lastName": "Taylor", "avatarUrl": null, @@ -3290,7 +3290,7 @@ "text": "Paul J. Deitel really shines in 'C++' with *Publisher's, description:*, Introduction. This technology novel uses elements and themes from the description with clear author voice." }, { - "userId": "bdaf508c-e836-44aa-a42e-7bfdd4cbb365", + "userId": "technology-user-5", "firstName": "Reagan", "lastName": "Clark", "avatarUrl": null, @@ -3300,7 +3300,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "7b550357-ddd4-45c3-9308-39e54fbeafc5", + "userId": "technology-user-4", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=7b550357-ddd4-45c3-9308-39e54fbeafc5", @@ -3310,7 +3310,7 @@ "text": "Okay read, nothing special." }, { - "userId": "77d7cd1c-6f16-4b6d-bd6e-8f3d2216e227", + "userId": "technology-user-1", "firstName": "Jordan", "lastName": "Lee", "avatarUrl": null, @@ -3320,7 +3320,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "6eebf64c-cb19-402d-92d5-2c01b10c5005", + "userId": "technology-user-2", "firstName": "Dakota", "lastName": "Turner", "avatarUrl": "https://i.pravatar.cc/150?u=6eebf64c-cb19-402d-92d5-2c01b10c5005", @@ -3330,7 +3330,7 @@ "text": "This book has good atmosphere, and the Technology influence is evident. It reminded me of the description's highlights." }, { - "userId": "e2dc1af7-db51-444d-a92c-ee2dfae863d5", + "userId": "biography-user-5", "firstName": "Quinn", "lastName": "White", "avatarUrl": "https://i.pravatar.cc/150?u=e2dc1af7-db51-444d-a92c-ee2dfae863d5", @@ -3340,7 +3340,7 @@ "text": "This book has good atmosphere, and the Education influence is evident. It reminded me of the description's highlights." }, { - "userId": "a6a0b256-7ad1-4238-bac0-d68ff3ff75d2", + "userId": "biography-user-3", "firstName": "Casey", "lastName": "Wilson", "avatarUrl": "https://i.pravatar.cc/150?u=a6a0b256-7ad1-4238-bac0-d68ff3ff75d2", @@ -3350,7 +3350,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "a3150bb0-c414-4042-a880-de1d97e334a5", + "userId": "biography-user-1", "firstName": "Shawn", "lastName": "Adams", "avatarUrl": "https://i.pravatar.cc/150?u=a3150bb0-c414-4042-a880-de1d97e334a5", @@ -3360,7 +3360,7 @@ "text": "William Shakespeare really shines in 'Julius Caesar' with Presents, original, text. This biography novel uses Education elements and themes from the description with clear author voice." }, { - "userId": "cf69e9ad-f704-4f29-a5c9-008d665d90c8", + "userId": "biography-user-5", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": "https://i.pravatar.cc/150?u=cf69e9ad-f704-4f29-a5c9-008d665d90c8", @@ -3370,7 +3370,7 @@ "text": "Okay read, nothing special." }, { - "userId": "f0df07e1-702f-46ee-af0e-b1362ca8cc9e", + "userId": "mixed-reader-2", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=f0df07e1-702f-46ee-af0e-b1362ca8cc9e", @@ -3380,7 +3380,7 @@ "text": "This book has good atmosphere, and the Education influence is evident. It reminded me of the description's highlights." }, { - "userId": "1263cb5d-79b7-4fe2-953e-5652c2493416", + "userId": "biography-user-4", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=1263cb5d-79b7-4fe2-953e-5652c2493416", @@ -3390,7 +3390,7 @@ "text": "Frederick Douglass really shines in 'Narrative of the life of Frederick Douglass' with This, book, autobiographical. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "31ace216-0c98-423c-a85e-bc4fc7f10800", + "userId": "biography-user-1", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=31ace216-0c98-423c-a85e-bc4fc7f10800", @@ -3400,7 +3400,7 @@ "text": "I enjoyed the biography style, especially the parts that echo Biography. Not a perfect read but very worthwhile." }, { - "userId": "14bdd3d9-fd64-452e-8ec5-c79509fbc143", + "userId": "biography-user-3", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": null, @@ -3410,7 +3410,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "1a7432e0-cfae-41fe-b866-401ffb114aa9", + "userId": "biography-user-2", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=1a7432e0-cfae-41fe-b866-401ffb114aa9", @@ -3420,7 +3420,7 @@ "text": "Okay read, nothing special." }, { - "userId": "bf827f4f-ef8f-442c-8247-95af96e7d409", + "userId": "biography-user-1", "firstName": "Dallas", "lastName": "Rodriguez", "avatarUrl": "https://i.pravatar.cc/150?u=bf827f4f-ef8f-442c-8247-95af96e7d409", @@ -3430,7 +3430,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "6936c579-e1ab-4227-b181-a9879be0b173", + "userId": "biography-user-1", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=6936c579-e1ab-4227-b181-a9879be0b173", @@ -3440,7 +3440,7 @@ "text": "Solomon Northup really shines in 'Twelve years a slave' with Twelve, Years, Slave. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "4473d17d-2b1b-4ee1-b036-2a468f34c458", + "userId": "mixed-reader-4", "firstName": "Taylor", "lastName": "Brown", "avatarUrl": "https://i.pravatar.cc/150?u=4473d17d-2b1b-4ee1-b036-2a468f34c458", @@ -3450,7 +3450,7 @@ "text": "This book has good atmosphere, and the Biography influence is evident. It reminded me of the description's highlights." }, { - "userId": "b93ee634-fe4f-49d2-81ed-886c5915eca0", + "userId": "biography-user-2", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=b93ee634-fe4f-49d2-81ed-886c5915eca0", @@ -3460,7 +3460,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "00906352-cb72-45dc-add5-2c564f347d64", + "userId": "biography-user-1", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": "https://i.pravatar.cc/150?u=00906352-cb72-45dc-add5-2c564f347d64", @@ -3470,7 +3470,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "18bc3e05-9643-4ec2-8db6-d049731f118c", + "userId": "biography-user-3", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": null, @@ -3480,7 +3480,7 @@ "text": "Okay read, nothing special." }, { - "userId": "a3f3a04d-aed1-42b6-8a9b-1fed31222e3a", + "userId": "biography-user-3", "firstName": "Remy", "lastName": "Roberts", "avatarUrl": "https://i.pravatar.cc/150?u=a3f3a04d-aed1-42b6-8a9b-1fed31222e3a", @@ -3490,7 +3490,7 @@ "text": "Willa Cather really shines in 'My Ántonia' with Antonia, first, published. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "7c132812-b0f2-4b38-8d7d-9984b3e6426a", + "userId": "biography-user-3", "firstName": "Rowan", "lastName": "Green", "avatarUrl": null, @@ -3500,7 +3500,7 @@ "text": "Average book, use your judgment." }, { - "userId": "bcfda15b-b300-415d-b29d-5ca2b9faaa84", + "userId": "mixed-reader-1", "firstName": "Hayden", "lastName": "Jackson", "avatarUrl": "https://i.pravatar.cc/150?u=bcfda15b-b300-415d-b29d-5ca2b9faaa84", @@ -3510,7 +3510,7 @@ "text": "I enjoyed the biography style, especially the parts that echo Biography. Not a perfect read but very worthwhile." }, { - "userId": "b8248fd0-15e1-42ed-92c6-897d9bdde005", + "userId": "biography-user-5", "firstName": "Avery", "lastName": "Thompson", "avatarUrl": "https://i.pravatar.cc/150?u=b8248fd0-15e1-42ed-92c6-897d9bdde005", @@ -3520,7 +3520,7 @@ "text": "The plot of 'My Ántonia' has strong Biography moments; the characters are well-developed and the pacing is solid." }, { - "userId": "d4803ff1-c77f-47d8-a7ab-7664fade4999", + "userId": "biography-user-5", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=d4803ff1-c77f-47d8-a7ab-7664fade4999", @@ -3530,7 +3530,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "82e1b73e-cfe2-4162-bfe3-b070a69c79a9", + "userId": "biography-user-3", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": null, @@ -3540,7 +3540,7 @@ "text": "Geoffrey Chaucer really shines in 'The Canterbury Tales' with collection, stories, written. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "d4d02153-70f0-4b3f-b301-81c5969265e2", + "userId": "biography-user-5", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=d4d02153-70f0-4b3f-b301-81c5969265e2", @@ -3550,7 +3550,7 @@ "text": "Good book, very nice." }, { - "userId": "0b0bc5d5-9c5b-4ccb-ae4d-cff881efc31e", + "userId": "biography-user-2", "firstName": "Peyton", "lastName": "Phillips", "avatarUrl": "https://i.pravatar.cc/150?u=0b0bc5d5-9c5b-4ccb-ae4d-cff881efc31e", @@ -3560,7 +3560,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "44b3b137-e752-4039-8d24-aa56b50cfe8c", + "userId": "biography-user-4", "firstName": "Brooklyn", "lastName": "Young", "avatarUrl": null, @@ -3570,7 +3570,7 @@ "text": "This book has good atmosphere, and the Biography influence is evident. It reminded me of the description's highlights." }, { - "userId": "b3f67ccc-af4b-482b-8c62-c2b27bf20611", + "userId": "biography-user-2", "firstName": "Casey", "lastName": "Wilson", "avatarUrl": "https://i.pravatar.cc/150?u=b3f67ccc-af4b-482b-8c62-c2b27bf20611", @@ -3580,7 +3580,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "8b9770cd-10dc-4387-b374-9d19eb12f10c", + "userId": "biography-user-3", "firstName": "Devon", "lastName": "Hall", "avatarUrl": "https://i.pravatar.cc/150?u=8b9770cd-10dc-4387-b374-9d19eb12f10c", @@ -3590,7 +3590,7 @@ "text": "I enjoyed the biography style, especially the parts that echo Biography. Not a perfect read but very worthwhile." }, { - "userId": "df33a445-47ca-4064-9024-9aad825ac5c3", + "userId": "mixed-reader-3", "firstName": "Kendall", "lastName": "Lewis", "avatarUrl": "https://i.pravatar.cc/150?u=df33a445-47ca-4064-9024-9aad825ac5c3", @@ -3600,7 +3600,7 @@ "text": "William Shakespeare really shines in 'Sonnets' with feel, that, have. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "05212d3f-2dab-44f9-9dcf-29c56d7d8415", + "userId": "biography-user-4", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": "https://i.pravatar.cc/150?u=05212d3f-2dab-44f9-9dcf-29c56d7d8415", @@ -3610,7 +3610,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "ba640810-49ea-4736-b54c-0a5ec4297ba7", + "userId": "mixed-reader-3", "firstName": "Devon", "lastName": "Harris", "avatarUrl": "https://i.pravatar.cc/150?u=ba640810-49ea-4736-b54c-0a5ec4297ba7", @@ -3620,7 +3620,7 @@ "text": "Average book, use your judgment." }, { - "userId": "d5f3dbee-6aaa-41c1-875c-0c314c6fcb05", + "userId": "biography-user-2", "firstName": "Finley", "lastName": "Wright", "avatarUrl": "https://i.pravatar.cc/150?u=d5f3dbee-6aaa-41c1-875c-0c314c6fcb05", @@ -3630,7 +3630,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "6cf5cd5b-669d-40d6-9926-9a52ef10ed07", + "userId": "biography-user-3", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=6cf5cd5b-669d-40d6-9926-9a52ef10ed07", @@ -3640,7 +3640,7 @@ "text": "This book has good atmosphere, and the Biography influence is evident. It reminded me of the description's highlights." }, { - "userId": "160558da-c574-4737-8658-d5eab8d9ce23", + "userId": "biography-user-1", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": null, @@ -3650,7 +3650,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "1056171a-7f55-48ac-ac24-14aeafb11932", + "userId": "biography-user-4", "firstName": "Shawn", "lastName": "Adams", "avatarUrl": "https://i.pravatar.cc/150?u=1056171a-7f55-48ac-ac24-14aeafb11932", @@ -3660,7 +3660,7 @@ "text": "Mohandas Karamchand Gandhi really shines in 'An autobiography' with Gandhi's, non-violent, struggles. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "6800881f-3a42-4a8f-8bee-2d5b0042c17e", + "userId": "biography-user-4", "firstName": "Logan", "lastName": "Hill", "avatarUrl": "https://i.pravatar.cc/150?u=6800881f-3a42-4a8f-8bee-2d5b0042c17e", @@ -3670,7 +3670,7 @@ "text": "This book has good atmosphere, and the Biography influence is evident. It reminded me of the description's highlights." }, { - "userId": "838f936c-9440-4738-8009-7eb29dd92815", + "userId": "non-fiction-user-4", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=838f936c-9440-4738-8009-7eb29dd92815", @@ -3680,7 +3680,7 @@ "text": "Average book, use your judgment." }, { - "userId": "f51eff27-d066-40cf-9bf0-81b7b4d24434", + "userId": "mixed-reader-5", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": null, @@ -3690,7 +3690,7 @@ "text": "This book has good atmosphere, and the Fantasy, Classic influence is evident. It reminded me of the description's highlights." }, { - "userId": "00871b6b-c46a-4913-a808-1b6955055a55", + "userId": "non-fiction-user-4", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": null, @@ -3700,7 +3700,7 @@ "text": "The plot of 'The Secret Garden' has strong Fantasy, Classic moments; the characters are well-developed and the pacing is solid." }, { - "userId": "9e187a67-0dc9-4105-98c9-47d221a8079a", + "userId": "mixed-reader-5", "firstName": "Devon", "lastName": "Hall", "avatarUrl": "https://i.pravatar.cc/150?u=9e187a67-0dc9-4105-98c9-47d221a8079a", @@ -3710,7 +3710,7 @@ "text": "Frances Hodgson Burnett really shines in 'The Secret Garden' with ten-year-old, orphan, comes. This non-fiction novel uses Fantasy, Classic elements and themes from the description with clear author voice." }, { - "userId": "f6368ac8-eab0-44f0-95e3-ae2e9fd16feb", + "userId": "mixed-reader-1", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=f6368ac8-eab0-44f0-95e3-ae2e9fd16feb", @@ -3720,7 +3720,7 @@ "text": "Okay read, nothing special." }, { - "userId": "72c68070-fd78-4a05-b201-fae283828dfc", + "userId": "non-fiction-user-4", "firstName": "Quinn", "lastName": "White", "avatarUrl": "https://i.pravatar.cc/150?u=72c68070-fd78-4a05-b201-fae283828dfc", @@ -3730,7 +3730,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "c580a149-0b54-4333-9ca0-9764f398efc9", + "userId": "non-fiction-user-4", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": null, @@ -3740,7 +3740,7 @@ "text": "Gustave Flaubert really shines in 'Madame Bovary' with Charles, Bovary, médecin. This non-fiction novel uses elements and themes from the description with clear author voice." }, { - "userId": "8d3bfad8-6101-4f3d-bff5-de1139466937", + "userId": "non-fiction-user-1", "firstName": "Jamie", "lastName": "Taylor", "avatarUrl": null, @@ -3750,7 +3750,7 @@ "text": "The plot of 'Madame Bovary' has strong Non-Fiction moments; the characters are well-developed and the pacing is solid." }, { - "userId": "59dbe493-9ed6-4695-b7b6-350f4198f53f", + "userId": "non-fiction-user-4", "firstName": "Finley", "lastName": "Wright", "avatarUrl": "https://i.pravatar.cc/150?u=59dbe493-9ed6-4695-b7b6-350f4198f53f", @@ -3760,7 +3760,7 @@ "text": "孙武 really shines in 'The Art of War' with ancient, Chinese, military. This non-fiction novel uses elements and themes from the description with clear author voice." }, { - "userId": "9899e074-fd45-4bbd-afbe-0960c8449212", + "userId": "non-fiction-user-5", "firstName": "Riley", "lastName": "Moore", "avatarUrl": "https://i.pravatar.cc/150?u=9899e074-fd45-4bbd-afbe-0960c8449212", @@ -3770,7 +3770,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "d4799d63-660d-4197-a1d6-367ec4011ee9", + "userId": "mixed-reader-4", "firstName": "Aiden", "lastName": "Allen", "avatarUrl": "https://i.pravatar.cc/150?u=d4799d63-660d-4197-a1d6-367ec4011ee9", @@ -3780,7 +3780,7 @@ "text": "Not my genre, but still pretty good." }, { - "userId": "270e718c-c945-46d3-95c7-d65b681cc187", + "userId": "non-fiction-user-5", "firstName": "Alex", "lastName": "Smith", "avatarUrl": "https://i.pravatar.cc/150?u=270e718c-c945-46d3-95c7-d65b681cc187", @@ -3790,7 +3790,7 @@ "text": "The plot of 'The Art of War' has strong Non-Fiction moments; the characters are well-developed and the pacing is solid." }, { - "userId": "b4f29e82-2b68-4d59-a74c-3d7a7e109eef", + "userId": "non-fiction-user-2", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=b4f29e82-2b68-4d59-a74c-3d7a7e109eef", @@ -3800,7 +3800,7 @@ "text": "This book has good atmosphere, and the Non-Fiction influence is evident. It reminded me of the description's highlights." }, { - "userId": "b65cadb8-faf4-4524-a0ec-afc356962410", + "userId": "non-fiction-user-4", "firstName": "Morgan", "lastName": "Davis", "avatarUrl": null, @@ -3810,7 +3810,7 @@ "text": "Фёдор Михайлович Достоевский really shines in 'Записки изъ подполья' with nameless, hero, profoundly. This non-fiction novel uses elements and themes from the description with clear author voice." }, { - "userId": "05fb8716-faec-425c-9621-38f651eca34d", + "userId": "non-fiction-user-1", "firstName": "Cameron", "lastName": "Anderson", "avatarUrl": "https://i.pravatar.cc/150?u=05fb8716-faec-425c-9621-38f651eca34d", @@ -3820,7 +3820,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "1d30c267-9a4f-4b61-a30d-03750719bba5", + "userId": "non-fiction-user-5", "firstName": "Alex", "lastName": "Smith", "avatarUrl": "https://i.pravatar.cc/150?u=1d30c267-9a4f-4b61-a30d-03750719bba5", @@ -3830,7 +3830,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "fb05e457-d0bc-4c4a-ab6c-52f338a3def3", + "userId": "non-fiction-user-3", "firstName": "Parker", "lastName": "Thomas", "avatarUrl": "https://i.pravatar.cc/150?u=fb05e457-d0bc-4c4a-ab6c-52f338a3def3", @@ -3840,7 +3840,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "372f994b-dcda-4723-acc6-4bdf9fcdef09", + "userId": "non-fiction-user-3", "firstName": "Alex", "lastName": "Smith", "avatarUrl": null, @@ -3850,7 +3850,7 @@ "text": "I enjoyed the non-fiction style, especially the parts that echo Education. Not a perfect read but very worthwhile." }, { - "userId": "04424dd9-7a59-4832-9465-c3442de78f4b", + "userId": "non-fiction-user-5", "firstName": "Rowan", "lastName": "Garcia", "avatarUrl": "https://i.pravatar.cc/150?u=04424dd9-7a59-4832-9465-c3442de78f4b", @@ -3860,7 +3860,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "a61546b6-afa6-4485-b149-c42347849a13", + "userId": "non-fiction-user-2", "firstName": "Blake", "lastName": "Walker", "avatarUrl": "https://i.pravatar.cc/150?u=a61546b6-afa6-4485-b149-c42347849a13", @@ -3870,7 +3870,7 @@ "text": "Not my genre, but still pretty good." }, { - "userId": "0afee10a-9d10-4353-9f4b-1e36524d8d29", + "userId": "non-fiction-user-5", "firstName": "Rowan", "lastName": "Green", "avatarUrl": null, @@ -3880,7 +3880,7 @@ "text": "Πλάτων really shines in 'πολιτεία' with Republic, Plato's, most. This non-fiction novel uses Education elements and themes from the description with clear author voice." }, { - "userId": "d378ad7a-600d-4637-aa9b-b5372964a838", + "userId": "non-fiction-user-3", "firstName": "Jamie", "lastName": "Taylor", "avatarUrl": null, @@ -3890,7 +3890,7 @@ "text": "Good book, very nice." }, { - "userId": "e38c50b1-bce1-4f55-a19e-2f32a3a375db", + "userId": "non-fiction-user-1", "firstName": "Avery", "lastName": "Thompson", "avatarUrl": null, @@ -3900,7 +3900,7 @@ "text": "老子 really shines in 'Tao te Ching' with Within, ancient, Chinese. This non-fiction novel uses elements and themes from the description with clear author voice." }, { - "userId": "1bfc71c5-11c6-418f-9c58-bd385df919bd", + "userId": "mixed-reader-5", "firstName": "Cameron", "lastName": "Anderson", "avatarUrl": "https://i.pravatar.cc/150?u=1bfc71c5-11c6-418f-9c58-bd385df919bd", @@ -3910,7 +3910,7 @@ "text": "Nice story, enjoyed it." }, { - "userId": "3fa5b540-713f-4887-9a8e-d5cdb59f0779", + "userId": "mixed-reader-1", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": "https://i.pravatar.cc/150?u=3fa5b540-713f-4887-9a8e-d5cdb59f0779", @@ -3920,7 +3920,7 @@ "text": "Read it in one weekend. Good enough!" }, { - "userId": "66501615-2ab6-4c9a-87cf-507b70675db4", + "userId": "non-fiction-user-2", "firstName": "Riley", "lastName": "Moore", "avatarUrl": null, @@ -3930,7 +3930,7 @@ "text": "The ending was surprising, but the beginning was slow." }, { - "userId": "b27395f4-219b-47cf-b0ff-48b63b58a64a", + "userId": "mixed-reader-1", "firstName": "Dakota", "lastName": "Turner", "avatarUrl": null, @@ -3940,7 +3940,7 @@ "text": "This book has good atmosphere, and the Non-Fiction influence is evident. It reminded me of the description's highlights." }, { - "userId": "1e1c3752-45c6-4013-8dd8-cc4cda947653", + "userId": "mixed-reader-1", "firstName": "Marley", "lastName": "Nelson", "avatarUrl": "https://i.pravatar.cc/150?u=1e1c3752-45c6-4013-8dd8-cc4cda947653", @@ -3950,7 +3950,7 @@ "text": "I read this on vacation and it was fine." }, { - "userId": "88e32400-ed91-4431-b9fc-0e47b6f524b9", + "userId": "non-fiction-user-3", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": null, @@ -3960,7 +3960,7 @@ "text": "Friedrich Nietzsche really shines in 'Also sprach Zarathustra' with landmark, work, philosophy. This non-fiction novel uses Classic elements and themes from the description with clear author voice." }, { - "userId": "cec78f18-7d68-4282-8aa0-f3eff890ec90", + "userId": "non-fiction-user-4", "firstName": "Dylan", "lastName": "Carter", "avatarUrl": "https://i.pravatar.cc/150?u=cec78f18-7d68-4282-8aa0-f3eff890ec90", @@ -3970,7 +3970,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "b0571069-69b0-4804-97f5-1ed39ad5721c", + "userId": "mixed-reader-5", "firstName": "Remy", "lastName": "Roberts", "avatarUrl": null, @@ -3980,7 +3980,7 @@ "text": "Cool book, maybe a bit long, but still worth it." }, { - "userId": "623028f7-5e73-4ac3-a752-76d89c05a677", + "userId": "non-fiction-user-4", "firstName": "Morgan", "lastName": "Davis", "avatarUrl": "https://i.pravatar.cc/150?u=623028f7-5e73-4ac3-a752-76d89c05a677", @@ -3990,7 +3990,7 @@ "text": "This book has good atmosphere, and the Classic influence is evident. It reminded me of the description's highlights." }, { - "userId": "a1db471c-0e16-4228-8a15-b97093ce72ae", + "userId": "mixed-reader-4", "firstName": "Drew", "lastName": "King", "avatarUrl": "https://i.pravatar.cc/150?u=a1db471c-0e16-4228-8a15-b97093ce72ae", @@ -4000,7 +4000,7 @@ "text": "The plot of 'Romeo and Juliet' has strong Non-Fiction moments; the characters are well-developed and the pacing is solid." }, { - "userId": "4991fc15-cda2-420e-988e-fd7722232395", + "userId": "non-fiction-user-4", "firstName": "Casey", "lastName": "Wilson", "avatarUrl": "https://i.pravatar.cc/150?u=4991fc15-cda2-420e-988e-fd7722232395", @@ -4010,7 +4010,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "4e4b840d-d177-44e3-85a6-6e5ae76fed38", + "userId": "mixed-reader-5", "firstName": "Skyler", "lastName": "Martinez", "avatarUrl": null, @@ -4020,7 +4020,7 @@ "text": "William Shakespeare really shines in 'Romeo and Juliet' with Romeo, Juliet, tragedy. This non-fiction novel uses elements and themes from the description with clear author voice." }, { - "userId": "b8583067-9385-4d8e-84e3-35ea87c2f48e", + "userId": "non-fiction-user-3", "firstName": "Skyler", "lastName": "Scott", "avatarUrl": "https://i.pravatar.cc/150?u=b8583067-9385-4d8e-84e3-35ea87c2f48e", @@ -4030,7 +4030,7 @@ "text": "This book has good atmosphere, and the Non-Fiction influence is evident. It reminded me of the description's highlights." }, { - "userId": "3f03c8f5-2492-4634-b21c-357ce933fea7", + "userId": "non-fiction-user-5", "firstName": "Devon", "lastName": "Harris", "avatarUrl": null, @@ -4040,7 +4040,7 @@ "text": "Loved it! Would recommend to friends." }, { - "userId": "fc86e651-039d-481a-83c0-07119ce73ec8", + "userId": "biography-user-3", "firstName": "Cameron", "lastName": "Anderson", "avatarUrl": "https://i.pravatar.cc/150?u=fc86e651-039d-481a-83c0-07119ce73ec8", @@ -4050,7 +4050,7 @@ "text": "Richard Henry Dana really shines in 'Two years before the mast' with *Two, Years, before. This biography novel uses elements and themes from the description with clear author voice." }, { - "userId": "a207aea8-ec75-489b-adf1-0acdbc6b94c3", + "userId": "mixed-reader-4", "firstName": "Kai", "lastName": "Lopez", "avatarUrl": null, @@ -4060,7 +4060,7 @@ "text": "Okay read, nothing special." }, { - "userId": "5a7048bc-e9c4-4e15-999c-244c3fd06c0b", + "userId": "biography-user-3", "firstName": "Hunter", "lastName": "Perez", "avatarUrl": "https://i.pravatar.cc/150?u=5a7048bc-e9c4-4e15-999c-244c3fd06c0b", @@ -4070,7 +4070,7 @@ "text": "Average book, use your judgment." }, { - "userId": "3699061b-5a18-4d29-81d3-ec4e3cf80eb8", + "userId": "biography-user-3", "firstName": "Aiden", "lastName": "Allen", "avatarUrl": null, @@ -4080,7 +4080,7 @@ "text": "I enjoyed the biography style, especially the parts that echo Biography. Not a perfect read but very worthwhile." }, { - "userId": "6c188deb-59cc-45c1-a317-2867ff6a60fc", + "userId": "technology-user-1", "firstName": "Aiden", "lastName": "Allen", "avatarUrl": "https://i.pravatar.cc/150?u=6c188deb-59cc-45c1-a317-2867ff6a60fc", @@ -4090,7 +4090,7 @@ "text": "This book has good atmosphere, and the Education influence is evident. It reminded me of the description's highlights." }, { - "userId": "b27e3449-d254-4f07-b77b-504a82c64799", + "userId": "mixed-reader-5", "firstName": "Taylor", "lastName": "Brown", "avatarUrl": "https://i.pravatar.cc/150?u=b27e3449-d254-4f07-b77b-504a82c64799", @@ -4100,7 +4100,7 @@ "text": "Good book, very nice." }, { - "userId": "560caa4e-48ed-4daa-b23a-a8a74e748562", + "userId": "technology-user-4", "firstName": "Emerson", "lastName": "Lee", "avatarUrl": null, @@ -4110,7 +4110,7 @@ "text": "Gerard J. Tortora really shines in 'Principles of Anatomy and Physiology' with This, classic, text. This technology novel uses Education elements and themes from the description with clear author voice." }, { - "userId": "fbcea92d-6753-440d-9d87-b7ccd02a6d60", + "userId": "technology-user-2", "firstName": "Casey", "lastName": "Wilson", "avatarUrl": "https://i.pravatar.cc/150?u=fbcea92d-6753-440d-9d87-b7ccd02a6d60", @@ -4120,7 +4120,7 @@ "text": "This book has good atmosphere, and the Education influence is evident. It reminded me of the description's highlights." }, { - "userId": "59497891-c8e1-476c-a8da-8627363db35a", + "userId": "mixed-reader-3", "firstName": "Devon", "lastName": "Thomas", "avatarUrl": null, @@ -4130,7 +4130,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "283b22cc-17a7-4c45-a66a-cb5ea5df10aa", + "userId": "fiction-user-5", "firstName": "Cameron", "lastName": "Garcia", "avatarUrl": null, @@ -4140,7 +4140,7 @@ "text": "Amazing pace and character development." }, { - "userId": "2078d9f2-c975-47e0-8647-0eb072f8c345", + "userId": "fiction-user-1", "firstName": "Cameron", "lastName": "Perez", "avatarUrl": null, @@ -4150,7 +4150,7 @@ "text": "Loved the story, will read again." }, { - "userId": "e3fd1c13-ae1f-4039-88df-356bfe12a44e", + "userId": "mixed-reader-5", "firstName": "Morgan", "lastName": "White", "avatarUrl": null, @@ -4160,7 +4160,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "df0092ed-e2b7-4b7e-8831-f8e5f6e83772", + "userId": "mixed-reader-5", "firstName": "Rowan", "lastName": "Wilson", "avatarUrl": null, @@ -4170,7 +4170,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "635db28a-9ecd-42ac-b37d-6b870c523127", + "userId": "mixed-reader-1", "firstName": "Phoenix", "lastName": "Perez", "avatarUrl": null, @@ -4180,7 +4180,7 @@ "text": "Amazing pace and character development." }, { - "userId": "992d535f-c9f4-4c7e-9220-b65c3f4d937d", + "userId": "mixed-reader-3", "firstName": "Dallas", "lastName": "Taylor", "avatarUrl": null, @@ -4190,7 +4190,7 @@ "text": "Amazing pace and character development." }, { - "userId": "5aac0615-aab4-437f-86a6-4273f2d56e3e", + "userId": "fiction-user-2", "firstName": "Finley", "lastName": "Smith", "avatarUrl": null, @@ -4200,7 +4200,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "eae49e8b-0cb6-4dc6-a5fd-72c3fcedb094", + "userId": "mixed-reader-1", "firstName": "Phoenix", "lastName": "Jones", "avatarUrl": null, @@ -4210,7 +4210,7 @@ "text": "Loved the story, will read again." }, { - "userId": "ddcc12db-3289-4b1b-9a82-1581eb51f574", + "userId": "fiction-user-3", "firstName": "Quinn", "lastName": "Smith", "avatarUrl": null, @@ -4220,7 +4220,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "0bec7251-4ee3-4b9b-b66b-8a795ac39172", + "userId": "fiction-user-2", "firstName": "Drew", "lastName": "Hernandez", "avatarUrl": null, @@ -4230,7 +4230,7 @@ "text": "Loved the story, will read again." }, { - "userId": "0763ba1b-5171-4bc9-af05-f647dfe766c4", + "userId": "fiction-user-5", "firstName": "Cameron", "lastName": "Brown", "avatarUrl": null, @@ -4240,7 +4240,7 @@ "text": "Amazing pace and character development." }, { - "userId": "a4d15010-7843-451a-b7b4-1ad480c88c89", + "userId": "mixed-reader-2", "firstName": "Riley", "lastName": "Davis", "avatarUrl": null, @@ -4250,7 +4250,7 @@ "text": "Loved the story, will read again." }, { - "userId": "83e2c586-b278-475b-a8c6-7d1647dc9802", + "userId": "fiction-user-1", "firstName": "Jamie", "lastName": "Johnson", "avatarUrl": null, @@ -4260,7 +4260,7 @@ "text": "Boring and uninteresting." }, { - "userId": "2975d036-0b0f-4859-af42-c0776a84b320", + "userId": "fiction-user-1", "firstName": "Baylor", "lastName": "Garcia", "avatarUrl": null, @@ -4270,7 +4270,7 @@ "text": "Loved the story, will read again." }, { - "userId": "e581b1d2-9e8a-48a6-be72-5437916b2e5a", + "userId": "fiction-user-1", "firstName": "Kai", "lastName": "Harris", "avatarUrl": null, @@ -4280,7 +4280,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "25d20311-2ecb-4d27-8960-33036f9b22d3", + "userId": "fiction-user-3", "firstName": "Kendall", "lastName": "Johnson", "avatarUrl": null, @@ -4290,7 +4290,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "2e553535-6962-4006-ab5f-8b7912277dea", + "userId": "mixed-reader-3", "firstName": "Avery", "lastName": "Martinez", "avatarUrl": null, @@ -4300,7 +4300,7 @@ "text": "Boring and uninteresting." }, { - "userId": "d9ac48f0-94a6-4e7c-bfb9-990c1bbddbfa", + "userId": "mixed-reader-5", "firstName": "Devon", "lastName": "Wilson", "avatarUrl": null, @@ -4310,7 +4310,7 @@ "text": "Amazing pace and character development." }, { - "userId": "786f2718-1842-4487-997a-b5eac64130ed", + "userId": "non-fiction-user-4", "firstName": "Phoenix", "lastName": "White", "avatarUrl": null, @@ -4320,7 +4320,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "309d2805-0805-49ba-8b1a-fc822c1264c6", + "userId": "non-fiction-user-5", "firstName": "Parker", "lastName": "White", "avatarUrl": null, @@ -4330,7 +4330,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "b8e1c713-abcd-4c82-9985-ac135f3655b2", + "userId": "non-fiction-user-2", "firstName": "Kai", "lastName": "Smith", "avatarUrl": null, @@ -4340,7 +4340,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "554acb25-e5dd-42cd-9114-e9b9e2ab1936", + "userId": "mixed-reader-4", "firstName": "Reese", "lastName": "Clark", "avatarUrl": null, @@ -4350,7 +4350,7 @@ "text": "Amazing pace and character development." }, { - "userId": "c7e36d63-c910-4f00-bd79-6410b8a20db9", + "userId": "fiction-user-4", "firstName": "Baylor", "lastName": "Sanchez", "avatarUrl": null, @@ -4360,7 +4360,7 @@ "text": "Boring and uninteresting." }, { - "userId": "d1d6f189-6598-48fb-84da-b2178213f214", + "userId": "fiction-user-5", "firstName": "Alex", "lastName": "Martin", "avatarUrl": null, @@ -4370,7 +4370,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "b61b4d3c-8255-4d62-b318-7be687982802", + "userId": "fiction-user-2", "firstName": "Reese", "lastName": "Ramirez", "avatarUrl": null, @@ -4380,7 +4380,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "96d9735e-7e6d-46d5-9dfc-5aacb3efc209", + "userId": "fiction-user-5", "firstName": "Reagan", "lastName": "White", "avatarUrl": null, @@ -4390,7 +4390,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "355e9f2f-b2f7-40e3-ab9a-34eac93271dd", + "userId": "mixed-reader-3", "firstName": "Kendall", "lastName": "Wilson", "avatarUrl": null, @@ -4400,7 +4400,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "fc669b36-f461-491f-8d5b-c37b7b89f414", + "userId": "fiction-user-2", "firstName": "Phoenix", "lastName": "Thomas", "avatarUrl": null, @@ -4410,7 +4410,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "ada9853f-7c99-4cbd-b30e-469082f195d3", + "userId": "mixed-reader-1", "firstName": "Emerson", "lastName": "Smith", "avatarUrl": null, @@ -4420,7 +4420,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "4ea50d8b-1d35-493e-b797-be26377ecde1", + "userId": "fiction-user-5", "firstName": "Finley", "lastName": "Harris", "avatarUrl": null, @@ -4430,7 +4430,7 @@ "text": "Loved the story, will read again." }, { - "userId": "eb23b8b7-f586-4439-935f-df4bc7bbeec4", + "userId": "fiction-user-3", "firstName": "Baylor", "lastName": "Harris", "avatarUrl": null, @@ -4440,7 +4440,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "3334f2ed-e77e-4d47-b8ae-4ab39356045f", + "userId": "non-fiction-user-1", "firstName": "Blake", "lastName": "Williams", "avatarUrl": null, @@ -4450,7 +4450,7 @@ "text": "Amazing pace and character development." }, { - "userId": "844964a8-cdce-478f-acab-48bc809b0dd1", + "userId": "biography-user-4", "firstName": "Marley", "lastName": "Martin", "avatarUrl": null, @@ -4460,7 +4460,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "94888377-9579-4db8-bbd1-54447bd64eea", + "userId": "mixed-reader-2", "firstName": "Rowan", "lastName": "Davis", "avatarUrl": null, @@ -4470,7 +4470,7 @@ "text": "Amazing pace and character development." }, { - "userId": "054d839a-b73e-4fb9-b302-f522a75e8927", + "userId": "fiction-user-3", "firstName": "Casey", "lastName": "Ramirez", "avatarUrl": null, @@ -4480,7 +4480,7 @@ "text": "Amazing pace and character development." }, { - "userId": "4265ead0-d58b-41b0-8154-5977f172f075", + "userId": "fiction-user-5", "firstName": "Cameron", "lastName": "Johnson", "avatarUrl": null, @@ -4490,7 +4490,7 @@ "text": "Loved the story, will read again." }, { - "userId": "cb9d56af-5a84-4037-9d62-de747400e8f3", + "userId": "fiction-user-2", "firstName": "Jordan", "lastName": "Martinez", "avatarUrl": null, @@ -4500,7 +4500,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "16e855d7-f82b-4c0c-9213-1b7ac0790315", + "userId": "mixed-reader-1", "firstName": "Hayden", "lastName": "Taylor", "avatarUrl": null, @@ -4510,7 +4510,7 @@ "text": "Amazing pace and character development." }, { - "userId": "9f6b6410-3d1d-4711-b6c9-00d3a12d2fd2", + "userId": "fiction-user-5", "firstName": "Kai", "lastName": "Garcia", "avatarUrl": null, @@ -4520,7 +4520,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "9139d14c-ca30-41b2-a7c7-5f42c0509c64", + "userId": "non-fiction-user-4", "firstName": "Hunter", "lastName": "Moore", "avatarUrl": null, @@ -4530,7 +4530,7 @@ "text": "Loved the story, will read again." }, { - "userId": "20dc64d7-462c-4a5d-b325-dd040f10da32", + "userId": "mixed-reader-4", "firstName": "Reagan", "lastName": "Harris", "avatarUrl": null, @@ -4540,7 +4540,7 @@ "text": "Amazing pace and character development." }, { - "userId": "ae1efe15-1e76-4d12-9800-49310e953999", + "userId": "non-fiction-user-4", "firstName": "Skyler", "lastName": "Martin", "avatarUrl": null, @@ -4550,7 +4550,7 @@ "text": "Amazing pace and character development." }, { - "userId": "b22b4770-eee8-4cda-98ef-515169d0153f", + "userId": "non-fiction-user-2", "firstName": "Riley", "lastName": "Smith", "avatarUrl": null, @@ -4560,7 +4560,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "b113f63c-cd37-4b70-a071-7aed794c2197", + "userId": "non-fiction-user-4", "firstName": "Phoenix", "lastName": "Hernandez", "avatarUrl": null, @@ -4570,7 +4570,7 @@ "text": "Loved the story, will read again." }, { - "userId": "8c54ae69-56be-44d7-802b-36e3c84861bd", + "userId": "non-fiction-user-5", "firstName": "Baylor", "lastName": "Smith", "avatarUrl": null, @@ -4580,7 +4580,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "2571f48c-8672-4fb4-b7dd-387ee3b3b48b", + "userId": "mixed-reader-3", "firstName": "Jamie", "lastName": "Hernandez", "avatarUrl": null, @@ -4590,7 +4590,7 @@ "text": "Amazing pace and character development." }, { - "userId": "816dcbe8-2339-4e81-a81c-174dd1aba706", + "userId": "non-fiction-user-5", "firstName": "Rowan", "lastName": "Brown", "avatarUrl": null, @@ -4600,7 +4600,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "55fa3bfd-3295-435d-8762-ab995e29c844", + "userId": "non-fiction-user-4", "firstName": "Logan", "lastName": "Taylor", "avatarUrl": null, @@ -4610,7 +4610,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "909fdfe2-002b-40b9-92e0-80067bc9aaa6", + "userId": "non-fiction-user-1", "firstName": "Devon", "lastName": "Ramirez", "avatarUrl": null, @@ -4620,7 +4620,7 @@ "text": "Amazing pace and character development." }, { - "userId": "a2c96ebe-0720-4ffa-86ff-b40e34f206b6", + "userId": "non-fiction-user-2", "firstName": "Dylan", "lastName": "Miller", "avatarUrl": null, @@ -4630,7 +4630,7 @@ "text": "Boring and uninteresting." }, { - "userId": "e274f632-ef5e-4ed4-959f-ecd1de991aa0", + "userId": "fiction-user-5", "firstName": "Reese", "lastName": "Williams", "avatarUrl": null, @@ -4640,7 +4640,7 @@ "text": "Loved the story, will read again." }, { - "userId": "f7719e3a-7ece-4a85-a8cc-7326a73b1a14", + "userId": "non-fiction-user-2", "firstName": "Kendall", "lastName": "Sanchez", "avatarUrl": null, @@ -4650,7 +4650,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "7bba413f-ebce-42ba-8180-5a669eb0bfdf", + "userId": "non-fiction-user-3", "firstName": "Morgan", "lastName": "Taylor", "avatarUrl": null, @@ -4660,7 +4660,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "bf4a9d5a-e619-4579-a297-a93be64b1db8", + "userId": "non-fiction-user-5", "firstName": "Baylor", "lastName": "Martinez", "avatarUrl": null, @@ -4670,7 +4670,7 @@ "text": "Loved the story, will read again." }, { - "userId": "77317f6a-d14f-4026-8ae7-5300df77f231", + "userId": "non-fiction-user-3", "firstName": "Kai", "lastName": "Johnson", "avatarUrl": null, @@ -4680,7 +4680,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "96b2b86d-e02f-4607-a883-53dca37418e7", + "userId": "non-fiction-user-2", "firstName": "Rowan", "lastName": "Rodriguez", "avatarUrl": null, @@ -4690,7 +4690,7 @@ "text": "Loved the story, will read again." }, { - "userId": "bf936e01-5f59-4731-8e4f-ce8f6a05e3f6", + "userId": "mixed-reader-4", "firstName": "Parker", "lastName": "Garcia", "avatarUrl": null, @@ -4700,7 +4700,7 @@ "text": "Loved the story, will read again." }, { - "userId": "d62765df-6d30-4022-a404-3e017c1c2c49", + "userId": "mixed-reader-1", "firstName": "Cameron", "lastName": "Williams", "avatarUrl": null, @@ -4710,7 +4710,7 @@ "text": "Loved the story, will read again." }, { - "userId": "497a8725-ba02-45d0-a3de-7abd3fe51939", + "userId": "non-fiction-user-3", "firstName": "Dallas", "lastName": "Anderson", "avatarUrl": null, @@ -4720,7 +4720,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "4e18f670-0224-4868-a2c1-bbcaef99b997", + "userId": "non-fiction-user-2", "firstName": "Riley", "lastName": "Perez", "avatarUrl": null, @@ -4730,7 +4730,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "a847876f-4865-4e82-8440-921e303fd68c", + "userId": "non-fiction-user-5", "firstName": "Blake", "lastName": "Hernandez", "avatarUrl": null, @@ -4740,7 +4740,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "832f4a97-edd2-49b2-9d85-3ec206861228", + "userId": "non-fiction-user-5", "firstName": "Jamie", "lastName": "Davis", "avatarUrl": null, @@ -4750,7 +4750,7 @@ "text": "Boring and uninteresting." }, { - "userId": "3f71164b-e7bc-42e3-9f13-d5c406bdf545", + "userId": "non-fiction-user-4", "firstName": "Blake", "lastName": "White", "avatarUrl": null, @@ -4760,7 +4760,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "d97ade1c-ece9-4e8f-bf92-0b04f941b193", + "userId": "non-fiction-user-3", "firstName": "Phoenix", "lastName": "Rodriguez", "avatarUrl": null, @@ -4770,7 +4770,7 @@ "text": "Boring and uninteresting." }, { - "userId": "efdbd61b-cbf8-4f67-91e8-14fca17a827f", + "userId": "non-fiction-user-3", "firstName": "Dallas", "lastName": "Brown", "avatarUrl": null, @@ -4780,7 +4780,7 @@ "text": "Loved the story, will read again." }, { - "userId": "7a8259b2-2d55-42b8-88fd-47b5cf39ea8f", + "userId": "fiction-user-4", "firstName": "Drew", "lastName": "Brown", "avatarUrl": null, @@ -4790,7 +4790,7 @@ "text": "Loved the story, will read again." }, { - "userId": "bd0c23b1-f4c6-4c18-aa10-2f55ff639fa4", + "userId": "non-fiction-user-4", "firstName": "Taylor", "lastName": "Jones", "avatarUrl": null, @@ -4800,7 +4800,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "0e4617bc-8a76-472f-af39-766fdb017cb2", + "userId": "non-fiction-user-1", "firstName": "Blake", "lastName": "Thomas", "avatarUrl": null, @@ -4810,7 +4810,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "45b114e0-f04d-4d28-9ee0-f96d51092167", + "userId": "non-fiction-user-3", "firstName": "Parker", "lastName": "Davis", "avatarUrl": null, @@ -4820,7 +4820,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "a9d99ed6-e117-4578-92a9-a2431f4bf272", + "userId": "fiction-user-2", "firstName": "Riley", "lastName": "Rodriguez", "avatarUrl": null, @@ -4830,7 +4830,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "ceb11e28-284c-4d04-b33c-14447b79990e", + "userId": "mixed-reader-3", "firstName": "Baylor", "lastName": "Perez", "avatarUrl": null, @@ -4840,7 +4840,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "d7586f65-10eb-49d6-9cec-9e265e54d735", + "userId": "non-fiction-user-2", "firstName": "Dallas", "lastName": "Hernandez", "avatarUrl": null, @@ -4850,7 +4850,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "bbfca3bf-9790-4aaa-b4e2-7b501e63006c", + "userId": "non-fiction-user-1", "firstName": "Taylor", "lastName": "Martin", "avatarUrl": null, @@ -4860,7 +4860,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "70acf991-d0ef-41c2-b9fb-9203b62a93e4", + "userId": "non-fiction-user-5", "firstName": "Hayden", "lastName": "Jones", "avatarUrl": null, @@ -4870,7 +4870,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "575b17cf-2b4d-4de0-8ed6-7cbc7559c235", + "userId": "non-fiction-user-2", "firstName": "Reese", "lastName": "Garcia", "avatarUrl": null, @@ -4880,7 +4880,7 @@ "text": "Amazing pace and character development." }, { - "userId": "47bc35bb-a7ad-4a22-91db-788bac9282b7", + "userId": "mixed-reader-5", "firstName": "Devon", "lastName": "Lee", "avatarUrl": null, @@ -4890,7 +4890,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "030868bb-a9a0-426d-90a3-cd683bc81c13", + "userId": "biography-user-5", "firstName": "Cameron", "lastName": "Davis", "avatarUrl": null, @@ -4900,7 +4900,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "3e0c6ce4-a608-4ece-8f8f-d45aa2da4e0b", + "userId": "biography-user-5", "firstName": "Jamie", "lastName": "Lee", "avatarUrl": null, @@ -4910,7 +4910,7 @@ "text": "Boring and uninteresting." }, { - "userId": "85ed18b4-bde0-4039-bbda-1029c8b0271f", + "userId": "non-fiction-user-1", "firstName": "Morgan", "lastName": "Thompson", "avatarUrl": null, @@ -4920,7 +4920,7 @@ "text": "Amazing pace and character development." }, { - "userId": "04f01725-7c37-48a7-85c2-158a1b35b2bb", + "userId": "non-fiction-user-3", "firstName": "Dallas", "lastName": "Harris", "avatarUrl": null, @@ -4930,7 +4930,7 @@ "text": "Amazing pace and character development." }, { - "userId": "dc52b7a5-08e5-4047-bd19-093ce236d4c2", + "userId": "non-fiction-user-3", "firstName": "Parker", "lastName": "Perez", "avatarUrl": null, @@ -4940,7 +4940,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "64608eda-d0a1-47f7-9497-99b348196815", + "userId": "fiction-user-3", "firstName": "Emerson", "lastName": "Thompson", "avatarUrl": null, @@ -4950,7 +4950,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "0c7f8c60-e26f-40cc-90d6-fa4970e3f6d8", + "userId": "fiction-user-4", "firstName": "Morgan", "lastName": "Hernandez", "avatarUrl": null, @@ -4960,7 +4960,7 @@ "text": "Amazing pace and character development." }, { - "userId": "c03b2443-089b-40aa-8d20-992572cb8eda", + "userId": "non-fiction-user-1", "firstName": "Hayden", "lastName": "Anderson", "avatarUrl": null, @@ -4970,7 +4970,7 @@ "text": "Loved the story, will read again." }, { - "userId": "14264b32-e288-44e7-abe9-f2b0a56aa0bb", + "userId": "non-fiction-user-2", "firstName": "Riley", "lastName": "Johnson", "avatarUrl": null, @@ -4980,7 +4980,7 @@ "text": "Amazing pace and character development." }, { - "userId": "05170f0c-d488-4bd2-9b8f-f6450981df8c", + "userId": "non-fiction-user-1", "firstName": "Drew", "lastName": "Thompson", "avatarUrl": null, @@ -4990,7 +4990,7 @@ "text": "Loved the story, will read again." }, { - "userId": "90843d90-976d-4487-9b2c-fc5ca7f5faa5", + "userId": "fiction-user-5", "firstName": "Rowan", "lastName": "White", "avatarUrl": null, @@ -5000,7 +5000,7 @@ "text": "Boring and uninteresting." }, { - "userId": "7693904c-d40b-44f1-8a9e-87b13d3043db", + "userId": "fiction-user-4", "firstName": "Hunter", "lastName": "Hernandez", "avatarUrl": null, @@ -5010,7 +5010,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "dac925ba-68da-4c7a-aad9-76459e92cc04", + "userId": "mixed-reader-5", "firstName": "Emerson", "lastName": "Ramirez", "avatarUrl": null, @@ -5020,7 +5020,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "b460cee0-b321-4c82-9f1e-048ef7d04a57", + "userId": "fiction-user-2", "firstName": "Parker", "lastName": "Clark", "avatarUrl": null, @@ -5030,7 +5030,7 @@ "text": "Boring and uninteresting." }, { - "userId": "8c4ca259-3017-47cc-919d-61e7a4751c81", + "userId": "fiction-user-5", "firstName": "Rowan", "lastName": "Clark", "avatarUrl": null, @@ -5040,7 +5040,7 @@ "text": "Loved the story, will read again." }, { - "userId": "a64be151-7768-4fd5-9bec-7a820dac8dbe", + "userId": "non-fiction-user-1", "firstName": "Taylor", "lastName": "Wilson", "avatarUrl": null, @@ -5050,7 +5050,7 @@ "text": "Loved the story, will read again." }, { - "userId": "81637caf-7e7c-4048-8b78-8f5c3e17a695", + "userId": "fiction-user-3", "firstName": "Avery", "lastName": "Martin", "avatarUrl": null, @@ -5060,7 +5060,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "266f627a-8e09-4100-b19c-fc5260c88272", + "userId": "fiction-user-4", "firstName": "Jordan", "lastName": "Johnson", "avatarUrl": null, @@ -5070,7 +5070,7 @@ "text": "Loved the story, will read again." }, { - "userId": "0ee809a0-ac17-4c70-bdca-4d613d21c1d6", + "userId": "non-fiction-user-1", "firstName": "Reese", "lastName": "White", "avatarUrl": null, @@ -5080,7 +5080,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "15b47675-7e7f-4ace-b66c-276160e83686", + "userId": "non-fiction-user-1", "firstName": "Reagan", "lastName": "Rodriguez", "avatarUrl": null, @@ -5090,7 +5090,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "86f4794b-6149-4765-80fd-7051f67b857e", + "userId": "non-fiction-user-4", "firstName": "Devon", "lastName": "Anderson", "avatarUrl": null, @@ -5100,7 +5100,7 @@ "text": "Boring and uninteresting." }, { - "userId": "4210bb13-9c1e-41e7-89bd-06e3aa12726f", + "userId": "non-fiction-user-5", "firstName": "Alex", "lastName": "Hernandez", "avatarUrl": null, @@ -5110,7 +5110,7 @@ "text": "Boring and uninteresting." }, { - "userId": "5ece6539-f62b-4c0a-98e5-9c612717534a", + "userId": "non-fiction-user-3", "firstName": "Reese", "lastName": "Jones", "avatarUrl": null, @@ -5120,7 +5120,7 @@ "text": "Amazing pace and character development." }, { - "userId": "dd07caff-e9f9-4f9d-ae71-ebb2de7e59dd", + "userId": "mixed-reader-5", "firstName": "Devon", "lastName": "Thompson", "avatarUrl": null, @@ -5130,7 +5130,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "18e1790c-ae95-47c3-afee-57af438bde60", + "userId": "non-fiction-user-5", "firstName": "Taylor", "lastName": "Clark", "avatarUrl": null, @@ -5140,7 +5140,7 @@ "text": "Amazing pace and character development." }, { - "userId": "eb7fcdd8-ee5b-4efe-a5d6-92ea213c8e28", + "userId": "non-fiction-user-4", "firstName": "Reagan", "lastName": "Johnson", "avatarUrl": null, @@ -5150,7 +5150,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "ed0abc92-6b06-4c76-af5c-91614eb7c812", + "userId": "non-fiction-user-5", "firstName": "Marley", "lastName": "Williams", "avatarUrl": null, @@ -5160,7 +5160,7 @@ "text": "Boring and uninteresting." }, { - "userId": "db6f3ec0-0519-47cf-b164-8bd13416c461", + "userId": "technology-user-3", "firstName": "Drew", "lastName": "Clark", "avatarUrl": null, @@ -5170,7 +5170,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "ec72b51f-c237-4cf9-aa09-c4cf804b1eb1", + "userId": "technology-user-2", "firstName": "Kai", "lastName": "Sanchez", "avatarUrl": null, @@ -5180,7 +5180,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "d9c10806-bc6b-46e0-8013-248ef6aa5778", + "userId": "technology-user-5", "firstName": "Rowan", "lastName": "Martinez", "avatarUrl": null, @@ -5190,7 +5190,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "4bb36d30-7230-4d08-ba7c-c87fd85a13b9", + "userId": "technology-user-4", "firstName": "Kai", "lastName": "Rodriguez", "avatarUrl": null, @@ -5200,7 +5200,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "13f4cf04-648a-451e-a5cb-ce0759dadefc", + "userId": "technology-user-3", "firstName": "Hunter", "lastName": "Thompson", "avatarUrl": null, @@ -5210,7 +5210,7 @@ "text": "Boring and uninteresting." }, { - "userId": "ff8bb96b-f8db-4af0-b871-b581abaafd04", + "userId": "technology-user-1", "firstName": "Skyler", "lastName": "Wilson", "avatarUrl": null, @@ -5220,7 +5220,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "464f9550-2b00-4bff-bab7-674c65a7aa57", + "userId": "technology-user-4", "firstName": "Jordan", "lastName": "Wilson", "avatarUrl": null, @@ -5230,7 +5230,7 @@ "text": "Amazing pace and character development." }, { - "userId": "cf3ba958-2258-4445-b159-f98da3f39eb9", + "userId": "mixed-reader-1", "firstName": "Riley", "lastName": "Thompson", "avatarUrl": null, @@ -5240,7 +5240,7 @@ "text": "Boring and uninteresting." }, { - "userId": "cddce724-6d9b-4e3a-a194-24025dacad07", + "userId": "technology-user-3", "firstName": "Phoenix", "lastName": "Johnson", "avatarUrl": null, @@ -5250,7 +5250,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "951d3a14-d088-4b94-804c-31933a4674ad", + "userId": "technology-user-2", "firstName": "Alex", "lastName": "Anderson", "avatarUrl": null, @@ -5260,7 +5260,7 @@ "text": "Amazing pace and character development." }, { - "userId": "bd6bbc83-81ff-472f-a68d-bd585457f4a5", + "userId": "technology-user-3", "firstName": "Emerson", "lastName": "Garcia", "avatarUrl": null, @@ -5270,7 +5270,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "7894ef95-e1b1-4c95-a5bc-4f3838304044", + "userId": "technology-user-3", "firstName": "Alex", "lastName": "Thomas", "avatarUrl": null, @@ -5280,7 +5280,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "0e68abe1-a20b-4388-87b9-bd34c1d32acd", + "userId": "technology-user-4", "firstName": "Jordan", "lastName": "Ramirez", "avatarUrl": null, @@ -5290,7 +5290,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "95f75774-aec7-419f-b8c4-5b68bb028054", + "userId": "technology-user-1", "firstName": "Reagan", "lastName": "Brown", "avatarUrl": null, @@ -5300,7 +5300,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "e498946c-617f-417e-a081-1ce67155f61d", + "userId": "biography-user-4", "firstName": "Kai", "lastName": "Brown", "avatarUrl": null, @@ -5310,7 +5310,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "2c8e9c9f-d5a2-4d23-9244-3374c2150075", + "userId": "biography-user-4", "firstName": "Devon", "lastName": "Williams", "avatarUrl": null, @@ -5320,7 +5320,7 @@ "text": "Amazing pace and character development." }, { - "userId": "c7df0a7a-b958-4cfd-8722-e909035824f7", + "userId": "biography-user-3", "firstName": "Casey", "lastName": "Williams", "avatarUrl": null, @@ -5330,7 +5330,7 @@ "text": "Boring and uninteresting." }, { - "userId": "09ec676d-7ae4-429a-88cc-cbeecb087cf6", + "userId": "biography-user-2", "firstName": "Casey", "lastName": "Perez", "avatarUrl": null, @@ -5340,7 +5340,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "1ceaf0bb-2281-4782-8b95-145e958650ba", + "userId": "biography-user-5", "firstName": "Kendall", "lastName": "Davis", "avatarUrl": null, @@ -5350,7 +5350,7 @@ "text": "Boring and uninteresting." }, { - "userId": "cff58f20-e738-4e4a-8635-ce9892a1b86f", + "userId": "biography-user-1", "firstName": "Jordan", "lastName": "Harris", "avatarUrl": null, @@ -5360,7 +5360,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "c56b17b0-d772-4ebf-8026-3a633ffbdffc", + "userId": "biography-user-4", "firstName": "Logan", "lastName": "Clark", "avatarUrl": null, @@ -5370,7 +5370,7 @@ "text": "Boring and uninteresting." }, { - "userId": "88ca80e2-9118-4d60-8fbf-2dc52357a11a", + "userId": "mixed-reader-2", "firstName": "Cameron", "lastName": "Miller", "avatarUrl": null, @@ -5380,7 +5380,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "65b7e104-0e7d-423b-b0f3-a3c723304e61", + "userId": "biography-user-1", "firstName": "Taylor", "lastName": "Perez", "avatarUrl": null, @@ -5390,7 +5390,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "d340eda5-5bc5-4b61-8be4-e7ab32164030", + "userId": "biography-user-1", "firstName": "Emerson", "lastName": "Hernandez", "avatarUrl": null, @@ -5400,7 +5400,7 @@ "text": "Amazing pace and character development." }, { - "userId": "90892284-04eb-4010-a04a-b2737601c8cb", + "userId": "non-fiction-user-2", "firstName": "Baylor", "lastName": "Jones", "avatarUrl": null, @@ -5410,7 +5410,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "79cf72c4-3797-499d-86f4-be32ca6b7e4a", + "userId": "mixed-reader-2", "firstName": "Casey", "lastName": "Davis", "avatarUrl": null, @@ -5420,7 +5420,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "70ff9105-86e9-4004-86fd-aab4c3996c14", + "userId": "non-fiction-user-3", "firstName": "Blake", "lastName": "Davis", "avatarUrl": null, @@ -5430,7 +5430,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "a31c0d6f-7c99-40a6-8631-e90069bdc3c4", + "userId": "non-fiction-user-1", "firstName": "Dallas", "lastName": "Martin", "avatarUrl": null, @@ -5440,7 +5440,7 @@ "text": "Amazing pace and character development." }, { - "userId": "eccb4174-dfe9-4324-a36d-cb1d896d99d6", + "userId": "mixed-reader-5", "firstName": "Parker", "lastName": "Miller", "avatarUrl": null, @@ -5450,7 +5450,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "b780440d-2dfe-4f00-83bc-41738de2565d", + "userId": "mixed-reader-4", "firstName": "Dallas", "lastName": "Miller", "avatarUrl": null, @@ -5460,7 +5460,7 @@ "text": "Boring and uninteresting." }, { - "userId": "34712ed9-2ec9-4907-98b3-01625e8b5c37", + "userId": "non-fiction-user-2", "firstName": "Marley", "lastName": "Johnson", "avatarUrl": null, @@ -5470,7 +5470,7 @@ "text": "Boring and uninteresting." }, { - "userId": "f3b8a70b-8c8d-43f3-aace-44600a4dce0a", + "userId": "non-fiction-user-5", "firstName": "Quinn", "lastName": "Hernandez", "avatarUrl": null, @@ -5480,7 +5480,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "ef85d065-e25c-406f-8f31-a74d5b25162f", + "userId": "non-fiction-user-4", "firstName": "Parker", "lastName": "Thompson", "avatarUrl": null, @@ -5490,7 +5490,7 @@ "text": "Loved the story, will read again." }, { - "userId": "d2a7b883-c2e2-4aad-8600-7703bec6757e", + "userId": "non-fiction-user-2", "firstName": "Baylor", "lastName": "Lee", "avatarUrl": null, @@ -5500,7 +5500,7 @@ "text": "Loved the story, will read again." }, { - "userId": "b5488808-48a4-42bd-a266-40457e60b4af", + "userId": "non-fiction-user-5", "firstName": "Avery", "lastName": "Taylor", "avatarUrl": null, @@ -5510,7 +5510,7 @@ "text": "Too long and too slow for my preference." }, { - "userId": "4891d83c-91bc-4ee5-bfda-33bbcda0a347", + "userId": "non-fiction-user-2", "firstName": "Finley", "lastName": "Brown", "avatarUrl": null, @@ -5520,7 +5520,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "a9869556-8fce-4b93-95ce-2984f8c798e9", + "userId": "non-fiction-user-4", "firstName": "Quinn", "lastName": "Moore", "avatarUrl": null, @@ -5530,7 +5530,7 @@ "text": "This was okay overall, but not really my taste." }, { - "userId": "c4ec0383-d077-46f3-8dc0-b329970b16e7", + "userId": "non-fiction-user-1", "firstName": "Kendall", "lastName": "Clark", "avatarUrl": null, @@ -5540,7 +5540,7 @@ "text": "Amazing pace and character development." }, { - "userId": "d5773098-e2d4-41ef-aaf3-9e07cd79f69f", + "userId": "biography-user-1", "firstName": "Emerson", "lastName": "Martin", "avatarUrl": null, @@ -5550,7 +5550,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "7563acbd-86fe-49c8-8236-0d5d95455e41", + "userId": "biography-user-2", "firstName": "Avery", "lastName": "Ramirez", "avatarUrl": null, @@ -5560,7 +5560,7 @@ "text": "Great book, everything was perfect!" }, { - "userId": "b51f8fb1-ed7b-4052-8796-30d515f09c48", + "userId": "technology-user-4", "firstName": "Dallas", "lastName": "Sanchez", "avatarUrl": null, @@ -5570,7 +5570,7 @@ "text": "Not into it, but the style is nice." }, { - "userId": "2107871f-8c8d-434e-8541-0d7cb7c41041", + "userId": "technology-user-2", "firstName": "Alex", "lastName": "Lee", "avatarUrl": null, diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 47ed6e2..2c56799 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -20,6 +20,7 @@ services: build: /bookService environment: - SPRING_PROFILES_ACTIVE=prod + - RECOMMENDER_SERVICE_URL=http://recommender-service:8082 container_name: book-service ports: - "8080:8080" @@ -28,6 +29,8 @@ services: depends_on: mysql: condition: service_healthy + recommender-service: + condition: service_healthy networks: - e-library-network @@ -74,6 +77,29 @@ services: networks: - e-library-network + recommender-service: + build: + context: ./recommenderService + dockerfile: Dockerfile + container_name: recommender-service + ports: + - "8082:8082" + environment: + - ML_MODELS_DIR=/mnt/models + - LOG_LEVEL=INFO + - RECOMMENDER_SERVICE_PORT=8082 + - ENVIRONMENT=production + volumes: + - ./data/ml_models:/mnt/models + networks: + - e-library-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8082/api/v1/recommendations/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + prometheus: container_name: prometheus image: prom/prometheus:latest diff --git a/docker-compose.yml b/docker-compose.yml index d791088..dccf8de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,7 +45,7 @@ services: ports: - "27018:27017" volumes: - - mongodb-dev_data:/data/db + - ./docker/mongodb-dev/data:/data/db healthcheck: test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet networks: @@ -63,6 +63,7 @@ services: KC_DB_PASSWORD: password KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin + JAVA_OPTS_APPEND: -Djboss.as.management.blocking.timeout=3600 ports: - "8181:8080" volumes: @@ -108,11 +109,17 @@ services: depends_on: - broker ports: - - "8082:8082" + - "8085:8085" environment: SCHEMA_REGISTRY_HOST_NAME: schema-registry SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'broker:29092' - SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8082 + SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8085 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8085/subjects"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s networks: - e-library-network @@ -127,11 +134,35 @@ services: environment: KAFKA_CLUSTERS_NAME: local KAFKA_CLUSTERS_BOOTSTRAPSERVERS: broker:29092 - KAFKA_CLUSTERS_SCHEMAREGISTRY: http://schema-registry:8082 + KAFKA_CLUSTERS_SCHEMAREGISTRY: http://schema-registry:8085 DYNAMIC_CONFIG_ENABLED: 'true' networks: - e-library-network + recommender-service: + build: + context: ./recommenderService + dockerfile: Dockerfile + container_name: recommender-service + ports: + - "8082:8082" + environment: + - ML_MODELS_DIR=/mnt/models + - LOG_LEVEL=INFO + - RECOMMENDER_SERVICE_PORT=8082 + - KAFKA_BOOTSTRAP_SERVERS=broker:29092 + - KAFKA_SCHEMA_REGISTRY_URL=http://schema-registry:8085 + volumes: + - ./data/ml_models:/mnt/models + networks: + - e-library-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8082/api/v1/recommendations/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + prometheus: container_name: prometheus image: prom/prometheus:latest @@ -169,4 +200,3 @@ networks: volumes: mysql-dev_data: kc-mysql_data: - mongodb-dev_data: diff --git a/infrastructure/keycloak/e-library-realm.json b/infrastructure/keycloak/e-library-realm.json index 534ee12..91c85a9 100644 --- a/infrastructure/keycloak/e-library-realm.json +++ b/infrastructure/keycloak/e-library-realm.json @@ -82,6 +82,7 @@ "composite": true, "composites": { "realm": [ + "ROLE_USER", "offline_access", "uma_authorization" ], @@ -706,6 +707,7 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ + "http://localhost:9000/swagger-ui/*", "http://localhost:4200/*" ], "webOrigins": [ @@ -723,13 +725,20 @@ "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { - "oidc.ciba.grant.enabled": "false", "client.secret.creation.time": "1747844036", - "backchannel.logout.session.required": "true", "post.logout.redirect.uris": "+", - "display.on.consent.screen": "false", "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false" + "backchannel.logout.revoke.offline.tokens": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "client.use.lightweight.access.token.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "acr.loa.map": "{}", + "require.pushed.authorization.requests": "false", + "tls.client.certificate.bound.access.tokens": "false", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, @@ -1636,7 +1645,7 @@ ], "identityProviderMappers": [ { - "id": "9d3d8869-aff3-4392-99df-42cecaa0fed6", + "id": "e6526f94-8799-4350-8261-ce7a1bb2fafb", "name": "google-avatar", "identityProviderAlias": "google", "identityProviderMapper": "google-user-attribute-mapper", @@ -1724,14 +1733,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-full-name-mapper", "saml-user-attribute-mapper", - "saml-role-list-mapper", - "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", "oidc-address-mapper", - "oidc-usermodel-property-mapper" + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper" ] } }, @@ -1743,13 +1752,13 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", - "saml-user-attribute-mapper", - "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", - "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", "saml-role-list-mapper" ] } diff --git a/recommenderService/.dockerignore b/recommenderService/.dockerignore new file mode 100644 index 0000000..ac5c473 --- /dev/null +++ b/recommenderService/.dockerignore @@ -0,0 +1,14 @@ +__pycache__ +*.pyc +*.pyo +.pytest_cache +.env +.git +.venv +venv +*.egg-info +dist +build +tests +.gitignore +*.md diff --git a/recommenderService/Dockerfile b/recommenderService/Dockerfile new file mode 100644 index 0000000..7472994 --- /dev/null +++ b/recommenderService/Dockerfile @@ -0,0 +1,27 @@ +# Stage 1: Build/Runtime +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8082/api/v1/recommendations/health || exit 1 + +# Expose port +EXPOSE 8082 + +# Run application +CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"] diff --git a/recommenderService/config.py b/recommenderService/config.py new file mode 100644 index 0000000..ed5821e --- /dev/null +++ b/recommenderService/config.py @@ -0,0 +1,44 @@ +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# ML Models directory (shared volume in Docker) +ML_MODELS_DIR = os.getenv("ML_MODELS_DIR", "./models") + +# Logging configuration +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + +# Recommendation batch size (default top-K results) +BATCH_SIZE_FOR_RECOMMENDATIONS = 10 + +# Server port +PORT = int(os.getenv("RECOMMENDER_SERVICE_PORT", 8082)) +HOST = "0.0.0.0" + +# Enable local fallback to bookService popular_books.json +ENABLE_LOCAL_FALLBACK = True + +# Environment +ENV = os.getenv("ENVIRONMENT", "development") + +# Kafka Configuration (Event-driven review scoring) +KAFKA_BOOTSTRAP_SERVERS = os.getenv("KAFKA_BOOTSTRAP_SERVERS", "localhost:9092").split(",") +KAFKA_SCHEMA_REGISTRY_URL = os.getenv("KAFKA_SCHEMA_REGISTRY_URL", "http://localhost:8085") + +# Kafka Topics +KAFKA_REVIEW_SCORING_REQUEST_TOPIC = os.getenv("KAFKA_REVIEW_SCORING_REQUEST_TOPIC", "review-scoring-requests") +KAFKA_REVIEW_SCORING_RESULT_TOPIC = os.getenv("KAFKA_REVIEW_SCORING_RESULT_TOPIC", "review-scoring-results") +KAFKA_REVIEW_SCORING_DLQ_TOPIC = os.getenv("KAFKA_REVIEW_SCORING_DLQ_TOPIC", "review-scoring-dlq") + +# Kafka Consumer Group +KAFKA_CONSUMER_GROUP = os.getenv("KAFKA_CONSUMER_GROUP", "recommender-service-group") + +# Batch processing settings (for future optimization) +KAFKA_BATCH_SIZE = int(os.getenv("KAFKA_BATCH_SIZE", "1")) +KAFKA_BATCH_TIMEOUT_MS = int(os.getenv("KAFKA_BATCH_TIMEOUT_MS", "1000")) + +# Sentence transformer model for relevance scoring +SENTENCE_TRANSFORMER_MODEL = os.getenv("SENTENCE_TRANSFORMER_MODEL", "all-MiniLM-L6-v2") +SENTENCE_TRANSFORMER_MODEL_VERSION = "3.0.0" diff --git a/recommenderService/main.py b/recommenderService/main.py new file mode 100644 index 0000000..4e5fd24 --- /dev/null +++ b/recommenderService/main.py @@ -0,0 +1,108 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import os +import asyncio +from config import HOST, PORT, ML_MODELS_DIR +from utils.logger import logger +from services.ml_model_loader import MLModelLoader +from services.content_based_service import ContentBasedRecommender +from services.collaborative_service import CollaborativeRecommender +from services.hybrid_recommender import HybridRecommender +from services.kafka_consumer import start_review_scoring_consumer, stop_review_scoring_consumer +from routes import recommendations + +# Initialize ML loader as a module-level variable +ml_loader = MLModelLoader(models_dir=ML_MODELS_DIR) +recommender = None +kafka_consumer_task = None + +# Create FastAPI application +app = FastAPI( + title="e-library Recommender Service", + version="1.0.0", + description="Hybrid recommendation engine (content-based + collaborative filtering)" +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +async def startup_event(): + """Initialize ML models and start Kafka consumer on startup""" + global ml_loader, recommender, kafka_consumer_task + + logger.info("=" * 50) + logger.info("Starting recommenderService...") + logger.info("=" * 50) + + try: + # Load ML models + ml_loader.load_all_models() + + if not ml_loader.is_ready(): + raise RuntimeError("Failed to initialize ML models") + + # Initialize recommendation engines + content_based = ContentBasedRecommender(ml_loader) + collaborative = CollaborativeRecommender(ml_loader) + recommender = HybridRecommender(content_based, collaborative) + + # Set recommender context in routes + recommendations.set_recommender_context(ml_loader, recommender) + + # Start Kafka consumer for review scoring requests (async background task) + kafka_consumer_task = asyncio.create_task(start_review_scoring_consumer()) + logger.info("Review scoring consumer started in background") + + logger.info("=" * 50) + logger.info("recommenderService ready for requests") + logger.info("=" * 50) + + except Exception as e: + logger.error(f"Failed to start service: {e}") + raise + + +@app.on_event("shutdown") +async def shutdown_event(): + """Cleanup on shutdown""" + global kafka_consumer_task + logger.info("Shutting down recommenderService") + + # Stop Kafka consumer gracefully + try: + await stop_review_scoring_consumer() + except Exception as e: + logger.error(f"Error stopping Kafka consumer: {e}", exc_info=True) + + +@app.get("/") +async def root(): + """Root endpoint""" + return { + "service": "recommenderService", + "status": "running", + "version": "1.0.0" + } + + +# Include routers +app.include_router(recommendations.router) + + +if __name__ == "__main__": + import uvicorn + logger.info(f"Starting FastAPI server on {HOST}:{PORT}") + uvicorn.run( + app, + host=HOST, + port=PORT, + log_level="info" + ) diff --git a/recommenderService/models/__init__.py b/recommenderService/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/recommenderService/models/request_response_models.py b/recommenderService/models/request_response_models.py new file mode 100644 index 0000000..ff90e4b --- /dev/null +++ b/recommenderService/models/request_response_models.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Dict, Any +from enum import Enum + + +class ExplanationReasonType(str, Enum): + """Types of recommendation explanations""" + TF_IDF_MATCH = "TF_IDF_MATCH" + COLLABORATIVE_FILTER = "COLLABORATIVE_FILTER" + + +class ExplanationDetails(BaseModel): + """Why a book was recommended""" + primaryReason: str + reasonType: ExplanationReasonType + topContributors: List[str] = Field(default_factory=list) # Keywords or seed book IDs + confidenceScore: float + details: Optional[Dict[str, Any]] = None + + +class SimilarBookResponse(BaseModel): + """Response model for similar books""" + bookId: int + similarityScore: float + explanation: Optional[ExplanationDetails] = None + + +class PersonalizedRecommendationResponse(BaseModel): + """Response model for personalized recommendations""" + bookId: int + score: float + explanation: Optional[ExplanationDetails] = None + seedBookIds: Optional[List[int]] = None + + +class GetSimilarBooksRequest(BaseModel): + """Request model for getting similar books""" + bookId: int = Field(..., description="The book ID to find similar books for") + topK: int = Field(default=10, ge=1, le=50, description="Number of recommendations") + + +class GetPersonalizedRecommendationsRequest(BaseModel): + """Request model for getting personalized recommendations""" + userSeedBooks: List[int] = Field( + ..., + description="List of book IDs user has reviewed" + ) + topK: int = Field(default=10, ge=1, le=50, description="Number of recommendations") + + +class HealthCheckResponse(BaseModel): + """Health check response model""" + status: str + modelsLoaded: bool + modelMetadata: Optional[Dict[str, Any]] = None + + +class ErrorResponse(BaseModel): + """Error response model""" + error: str + detail: Optional[str] = None + status_code: int diff --git a/recommenderService/offline_evaluation.py b/recommenderService/offline_evaluation.py new file mode 100644 index 0000000..d3deaba --- /dev/null +++ b/recommenderService/offline_evaluation.py @@ -0,0 +1,1051 @@ +#!/usr/bin/env python3 +""" +Task 2.7: Offline Evaluation Script for Review Relevance Scoring System + +Purpose: + Validate review relevance scoring model quality on ground truth dataset + before integrating with live system. + +Usage: + python offline_evaluation.py \\ + --dataset phase1_datasets/ground_truth_evaluation_set.csv \\ + --model all-MiniLM-L6-v2 \\ + --output_dir evaluation_results/ \\ + --sample_size 10000 \\ + --score_threshold 0.6 + +Output: + - evaluation_report.html (interactive report) + - evaluation_metrics.json (machine-readable results) + - evaluation_log.txt (detailed processing log) + - flagged_outliers.csv (anomalous scores) + +Status: Phase 2, Task 2.7 - COMPLETE +""" + +import argparse +import csv +import json +import logging +import sys +import time +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import List, Tuple, Optional, Dict, Any +import math +from collections import defaultdict +import statistics + +import numpy as np +from sentence_transformers import SentenceTransformer +from sklearn.metrics import ( + precision_score, recall_score, f1_score, accuracy_score, + confusion_matrix, roc_auc_score, roc_curve +) + +# ============================================================================ +# Configuration & Data Classes +# ============================================================================ + +@dataclass +class EvaluationConfig: + """Configuration for offline evaluation""" + dataset_path: Path + model_name: str + output_dir: Path + sample_size: int + score_threshold: float + quick_test: bool = False + log_level: str = "INFO" + + def __post_init__(self): + """Validate configuration""" + if not self.dataset_path.exists(): + raise FileNotFoundError(f"Dataset not found: {self.dataset_path}") + self.output_dir.mkdir(parents=True, exist_ok=True) + + +@dataclass +class ScoringRecord: + """Single book-review pair scoring result""" + book_id: int + review_id: str + book_text: str # Aggregated metadata (Task 2.1) + review_text: str # Raw text + review_text_preprocessed: str # After Task 2.3 pipeline + book_embedding: np.ndarray + review_embedding: np.ndarray + raw_cosine_similarity: float + relevance_score: float # Normalized [0.0, 1.0] + confidence: float # [0.0, 1.0] + ground_truth_label: str # "RELEVANT" | "IRRELEVANT" + processing_time_ms: float + review_length: int + book_length: int + + +@dataclass +class EvaluationMetrics: + """Complete evaluation results""" + # Distribution metrics + mean_score: float + median_score: float + std_dev: float + min_score: float + max_score: float + score_histogram: Dict[str, int] # "0.0-0.1": count, ... + + # Quality metrics at threshold + threshold: float + accuracy: float + precision: float + recall: float + f1: float + roc_auc: float + + # Confusion matrix + true_positives: int + true_negatives: int + false_positives: int + false_negatives: int + + # Performance metrics + p50_latency_ms: float + p95_latency_ms: float + p99_latency_ms: float + total_runtime_sec: float + throughput_per_sec: float + + # Correlation metrics + correlation_score_to_label: float + + # Edge cases + null_score_count: int + short_review_count: int + long_review_count: int + poor_metadata_count: int + + # Pass/fail + passes_criteria: bool + failure_reasons: List[str] + + +# ============================================================================ +# Core Scoring Functions (from Tasks 2.1-2.5) +# ============================================================================ + +class BookMetadataAggregator: + """Task 2.1: Aggregate book metadata using Strategy C""" + + @staticmethod + def aggregate_from_dict(book_data: Dict[str, Any]) -> str: + """ + Aggregate book metadata (Strategy C: Selective Fields with Priority) + + Order: Title → Author → Genres → Category → Description + Skip NULL fields entirely + + Args: + book_data: dict with keys: title, author, genres, category, description + + Returns: + Aggregated text (max ~500 chars) + """ + parts = [] + + # Always include title + if book_data.get('title'): + parts.append(f"Title: {book_data['title']}") + + # Author (high signal) + if book_data.get('author'): + parts.append(f"Author: {book_data['author']}") + + # Genres (categorical signal) + if book_data.get('genres'): + genres = book_data['genres'] + if isinstance(genres, list): + genres = ', '.join(genres) + if genres: + parts.append(f"Genres: {genres}") + + # Category (categorical signal) + if book_data.get('category'): + parts.append(f"Category: {book_data['category']}") + + # Description (most semantic-rich, prioritize) + if book_data.get('description'): + parts.append(f"Description: {book_data['description']}") + + return '\n'.join(parts) + + +class ReviewTextPreprocessor: + """Task 2.3: Preprocess review text (7-step pipeline)""" + + @staticmethod + def preprocess(text: str) -> str: + """ + Preprocess review text using 7-step pipeline + + Steps: + 1. HTML decode (& → &, etc.) + 2. Lowercase + 3. Remove emojis/unicode + 4. Normalize whitespace + 5. Remove spoiler markers + 6. Collapse repeated punctuation + 7. Truncate at 1800 chars or 450 tokens + + Args: + text: Raw review text + + Returns: + Preprocessed text + """ + if not text: + return "" + + # Step 1: HTML decode + import html + text = html.unescape(text) + + # Step 2: Lowercase + text = text.lower() + + # Step 3: Remove emojis/unicode (keep ASCII + common punctuation) + text = ''.join( + c if ord(c) < 128 or c in '\n\t' else '' + for c in text + ) + + # Step 4: Normalize whitespace + text = ' '.join(text.split()) + + # Step 5: Remove spoiler markers + spoiler_patterns = [ + 'spoiler alert:', 'spoiler:', 'spoilers:', + 'trigger warning:', 'content warning:' + ] + for pattern in spoiler_patterns: + text = text.replace(pattern, '') + + # Step 6: Collapse repeated punctuation + while '!!' in text: + text = text.replace('!!', '!') + while '??' in text: + text = text.replace('??', '?') + while '..' in text: + text = text.replace('..', '.') + + # Step 7: Truncate at sentence boundary (~450 tokens = ~1800 chars) + max_chars = 1800 + if len(text) > max_chars: + # Find last sentence boundary + text = text[:max_chars] + last_period = max( + text.rfind('.'), + text.rfind('!'), + text.rfind('?') + ) + if last_period > max_chars - 200: # If near end + text = text[:last_period + 1] + + return text + + +class RelevanceScoringFunction: + """Task 2.5: Core relevance scoring function""" + + @staticmethod + def score_relevance( + book_embedding: np.ndarray, + review_embedding: np.ndarray, + text_quality: float = 0.5 + ) -> Tuple[float, float, float]: + """ + Compute relevance score from embeddings via cosine similarity + + Args: + book_embedding: Sentence transformer embedding for book + review_embedding: Sentence transformer embedding for review + text_quality: Quality score [0, 1] for confidence computation + + Returns: + Tuple of (relevance_score, raw_cosine_similarity, confidence) + - relevance_score: [0.0, 1.0] normalized + - raw_cosine_similarity: [-1.0, 1.0] before normalization + - confidence: [0.0, 1.0] confidence in score + """ + # Input validation + if book_embedding is None or review_embedding is None: + return None, None, None + + if len(book_embedding) == 0 or len(review_embedding) == 0: + return None, None, None + + # Compute cosine similarity + norm_book = np.linalg.norm(book_embedding) + norm_review = np.linalg.norm(review_embedding) + + if norm_book == 0 or norm_review == 0: + return None, None, None + + raw_cosine = np.dot(book_embedding, review_embedding) / (norm_book * norm_review) + raw_cosine = float(np.clip(raw_cosine, -1.0, 1.0)) + + # Normalize to [0, 1] + relevance_score = (raw_cosine + 1.0) / 2.0 + + # Confidence: high when |cosine_sim| is high (strong signal) + confidence = (abs(raw_cosine) * 0.7) + (text_quality * 0.3) + confidence = float(np.clip(confidence, 0.0, 1.0)) + + return relevance_score, raw_cosine, confidence + + +# ============================================================================ +# Dataset Loading +# ============================================================================ + +class GroundTruthDataset: + """Load and manage ground truth evaluation dataset""" + + def __init__(self, csv_path: Path, sample_size: int = 10000): + """ + Load ground truth dataset from CSV + + Expected columns: + - book_id: int + - review_id: string (UUID) + - book_text: string (aggregated metadata) + - review_text: string (raw review) + - review_rating: float (1-5) + - relevance_label: string ("RELEVANT" | "IRRELEVANT") + + Args: + csv_path: Path to ground_truth_evaluation_set.csv + sample_size: Max records to load (10000 for full, 100 for quick test) + """ + self.csv_path = csv_path + self.records = [] + self._load(sample_size) + + def _load(self, max_records: int): + """Load dataset from CSV""" + with open(self.csv_path, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + for i, row in enumerate(reader): + if i >= max_records: + break + + self.records.append({ + 'book_id': int(row['book_id']), + 'review_id': row['review_id'], + 'book_text': row.get('book_text', ''), + 'review_text': row.get('review_text', ''), + 'review_rating': float(row.get('review_rating', 3.0)), + 'relevance_label': row['relevance_label'], # Ground truth + }) + + def __len__(self) -> int: + return len(self.records) + + def __iter__(self): + return iter(self.records) + + +# ============================================================================ +# Evaluation Engine +# ============================================================================ + +class OfflineEvaluationEngine: + """Main evaluation orchestrator""" + + def __init__(self, config: EvaluationConfig, logger: logging.Logger): + self.config = config + self.logger = logger + self.model: Optional[SentenceTransformer] = None + self.scoring_records: List[ScoringRecord] = [] + self.metrics: Optional[EvaluationMetrics] = None + + def run(self) -> EvaluationMetrics: + """Execute full evaluation pipeline""" + start_time = time.time() + self.logger.info(f"Starting evaluation: {self.config.sample_size} samples") + + try: + # Step 1: Load model + self._load_model() + + # Step 2: Load dataset + dataset = self._load_dataset() + + # Step 3: Score all samples + self._score_all_samples(dataset) + + # Step 4: Compute metrics + self.metrics = self._compute_metrics() + + # Step 5: Generate report + self._generate_report() + + total_time = time.time() - start_time + self.logger.info(f"Evaluation complete in {total_time:.1f}s") + + return self.metrics + + except Exception as e: + self.logger.error(f"Evaluation failed: {e}", exc_info=True) + raise + + def _load_model(self): + """Load sentence transformer model""" + self.logger.info(f"Loading model: {self.config.model_name}") + try: + self.model = SentenceTransformer(self.config.model_name) + self.logger.info(f"Model loaded successfully") + except Exception as e: + self.logger.error(f"Failed to load model: {e}") + raise + + def _load_dataset(self) -> GroundTruthDataset: + """Load ground truth dataset""" + self.logger.info(f"Loading dataset: {self.config.dataset_path}") + dataset = GroundTruthDataset( + self.config.dataset_path, + sample_size=self.config.sample_size + ) + self.logger.info(f"Loaded {len(dataset)} samples") + return dataset + + def _score_all_samples(self, dataset: GroundTruthDataset): + """Score all book-review pairs and measure latency""" + start_time = time.time() + latencies = [] + + for i, record in enumerate(dataset): + sample_start = time.time() + + try: + # Preprocess review + review_preprocessed = ReviewTextPreprocessor.preprocess( + record['review_text'] + ) + + # Generate embeddings + book_embedding = self.model.encode(record['book_text']) + review_embedding = self.model.encode(review_preprocessed) + + # Score relevance + score, raw_cos, confidence = RelevanceScoringFunction.score_relevance( + book_embedding, + review_embedding, + text_quality=0.5 + ) + + # Record result + latency_ms = (time.time() - sample_start) * 1000 + latencies.append(latency_ms) + + self.scoring_records.append(ScoringRecord( + book_id=record['book_id'], + review_id=record['review_id'], + book_text=record['book_text'], + review_text=record['review_text'], + review_text_preprocessed=review_preprocessed, + book_embedding=book_embedding, + review_embedding=review_embedding, + raw_cosine_similarity=raw_cos if raw_cos is not None else float('nan'), + relevance_score=score if score is not None else float('nan'), + confidence=confidence if confidence is not None else float('nan'), + ground_truth_label=record['relevance_label'], + processing_time_ms=latency_ms, + review_length=len(record['review_text'].split()), + book_length=len(record['book_text'].split()), + )) + + except Exception as e: + self.logger.error(f"Failed to score sample {i}: {e}") + continue + + if (i + 1) % 100 == 0: + self.logger.info(f"Scored {i + 1}/{len(dataset)} samples") + + total_time = time.time() - start_time + self.logger.info(f"Scoring complete: {total_time:.1f}s for {len(self.scoring_records)} samples") + self.logger.info(f"Average latency: {np.mean(latencies):.1f}ms") + + def _compute_metrics(self) -> EvaluationMetrics: + """Compute all evaluation metrics""" + self.logger.info("Computing evaluation metrics...") + + # Extract data + scores = [r.relevance_score for r in self.scoring_records] + labels = [1 if r.ground_truth_label == "RELEVANT" else 0 + for r in self.scoring_records] + latencies = [r.processing_time_ms for r in self.scoring_records] + + # Remove NaN scores for metrics + valid_mask = np.array([not math.isnan(s) for s in scores]) + valid_scores = np.array(scores)[valid_mask] + valid_labels = np.array(labels)[valid_mask] + + # Distribution metrics + mean_score = float(np.mean(valid_scores)) + median_score = float(np.median(valid_scores)) + std_dev = float(np.std(valid_scores)) + min_score = float(np.min(valid_scores)) + max_score = float(np.max(valid_scores)) + + # Score histogram + histogram = defaultdict(int) + for score in valid_scores: + bin_idx = int(score * 10) # 0.0-0.1, 0.1-0.2, ... + bin_idx = min(bin_idx, 9) # Cap at 0.9-1.0 + histogram[f"{bin_idx * 0.1:.1f}-{(bin_idx + 1) * 0.1:.1f}"] += 1 + + # Compute binary predictions at threshold + predictions = np.array([1 if s >= self.config.score_threshold else 0 + for s in valid_scores]) + + # Quality metrics + cm = confusion_matrix(valid_labels, predictions) + tn, fp, fn, tp = cm.ravel() + + accuracy = accuracy_score(valid_labels, predictions) + precision = precision_score(valid_labels, predictions, zero_division=0) + recall = recall_score(valid_labels, predictions, zero_division=0) + f1 = f1_score(valid_labels, predictions, zero_division=0) + roc_auc = roc_auc_score(valid_labels, valid_scores) + + # Latency percentiles + p50 = float(np.percentile(latencies, 50)) + p95 = float(np.percentile(latencies, 95)) + p99 = float(np.percentile(latencies, 99)) + + # Throughput + total_time = sum(latencies) / 1000 # ms to seconds + throughput = len(self.scoring_records) / total_time if total_time > 0 else 0 + + # Correlation with label + try: + correlation = float(np.corrcoef(valid_scores, valid_labels)[0, 1]) + except: + correlation = 0.0 + + # Edge cases + null_count = sum(1 for s in scores if math.isnan(s)) + short_count = sum(1 for r in self.scoring_records if r.review_length < 5) + long_count = sum(1 for r in self.scoring_records if r.review_length > 300) + poor_metadata = sum(1 for r in self.scoring_records if r.book_length < 5) + + # Pass/fail criteria + failure_reasons = [] + passes = True + + if accuracy < 0.70: + failure_reasons.append(f"Accuracy {accuracy:.2%} < 0.70") + passes = False + if precision < 0.65: + failure_reasons.append(f"Precision {precision:.2%} < 0.65") + passes = False + if recall < 0.65: + failure_reasons.append(f"Recall {recall:.2%} < 0.65") + passes = False + if p95 > 500: + failure_reasons.append(f"P95 latency {p95:.0f}ms > 500ms") + passes = False + if null_count / len(scores) > 0.01: + failure_reasons.append(f"NULL scores {null_count / len(scores):.1%} > 1%") + passes = False + + metrics = EvaluationMetrics( + mean_score=mean_score, + median_score=median_score, + std_dev=std_dev, + min_score=min_score, + max_score=max_score, + score_histogram=dict(sorted(histogram.items())), + threshold=self.config.score_threshold, + accuracy=accuracy, + precision=precision, + recall=recall, + f1=f1, + roc_auc=roc_auc, + true_positives=int(tp), + true_negatives=int(tn), + false_positives=int(fp), + false_negatives=int(fn), + p50_latency_ms=p50, + p95_latency_ms=p95, + p99_latency_ms=p99, + total_runtime_sec=total_time, + throughput_per_sec=throughput, + correlation_score_to_label=correlation, + null_score_count=null_count, + short_review_count=short_count, + long_review_count=long_count, + poor_metadata_count=poor_metadata, + passes_criteria=passes, + failure_reasons=failure_reasons, + ) + + # Log results + self._log_metrics(metrics) + return metrics + + def _log_metrics(self, metrics: EvaluationMetrics): + """Log metrics to logger""" + self.logger.info("=" * 70) + self.logger.info("EVALUATION RESULTS") + self.logger.info("=" * 70) + self.logger.info(f"Samples evaluated: {len(self.scoring_records)}") + self.logger.info(f"Threshold: {metrics.threshold}") + self.logger.info("") + self.logger.info("DISTRIBUTION:") + self.logger.info(f" Mean score: {metrics.mean_score:.3f}") + self.logger.info(f" Median score: {metrics.median_score:.3f}") + self.logger.info(f" Std dev: {metrics.std_dev:.3f}") + self.logger.info(f" Range: [{metrics.min_score:.3f}, {metrics.max_score:.3f}]") + self.logger.info("") + self.logger.info("QUALITY METRICS:") + self.logger.info(f" Accuracy: {metrics.accuracy:.1%}") + self.logger.info(f" Precision: {metrics.precision:.1%}") + self.logger.info(f" Recall: {metrics.recall:.1%}") + self.logger.info(f" F1 Score: {metrics.f1:.3f}") + self.logger.info(f" ROC-AUC: {metrics.roc_auc:.3f}") + self.logger.info("") + self.logger.info("CONFUSION MATRIX:") + self.logger.info(f" TP: {metrics.true_positives}, FP: {metrics.false_positives}") + self.logger.info(f" FN: {metrics.false_negatives}, TN: {metrics.true_negatives}") + self.logger.info("") + self.logger.info("LATENCY:") + self.logger.info(f" P50: {metrics.p50_latency_ms:.1f}ms") + self.logger.info(f" P95: {metrics.p95_latency_ms:.1f}ms") + self.logger.info(f" P99: {metrics.p99_latency_ms:.1f}ms") + self.logger.info(f" Total runtime: {metrics.total_runtime_sec:.1f}s") + self.logger.info(f" Throughput: {metrics.throughput_per_sec:.1f} samples/sec") + self.logger.info("") + self.logger.info("EDGE CASES:") + self.logger.info(f" NULL scores: {metrics.null_score_count}") + self.logger.info(f" Short reviews (<5 words): {metrics.short_review_count}") + self.logger.info(f" Long reviews (>300 words): {metrics.long_review_count}") + self.logger.info(f" Poor metadata (<5 words): {metrics.poor_metadata_count}") + self.logger.info("") + + if metrics.passes_criteria: + self.logger.info("✅ PASS: All success criteria met!") + else: + self.logger.info("❌ FAIL: Some criteria not met:") + for reason in metrics.failure_reasons: + self.logger.info(f" - {reason}") + self.logger.info("=" * 70) + + def _generate_report(self): + """Generate evaluation reports""" + # JSON report + self._write_json_report() + + # HTML report + self._write_html_report() + + # Flagged outliers CSV + self._write_outliers_csv() + + self.logger.info(f"Reports written to: {self.config.output_dir}") + + def _write_json_report(self): + """Write machine-readable JSON report""" + report = { + 'timestamp': datetime.now().isoformat(), + 'config': { + 'dataset': str(self.config.dataset_path), + 'model': self.config.model_name, + 'sample_size': self.config.sample_size, + 'threshold': self.config.score_threshold, + }, + 'metrics': asdict(self.metrics), + } + + output_file = self.config.output_dir / 'evaluation_metrics.json' + with open(output_file, 'w') as f: + json.dump(report, f, indent=2, default=str) + + self.logger.info(f"JSON report: {output_file}") + + def _write_html_report(self): + """Write interactive HTML report""" + # Build histogram HTML + histogram_html = "
| Parameter | Value |
|---|---|
| Dataset | {self.config.dataset_path} |
| Model | {self.config.model_name} |
| Samples Evaluated | {len(self.scoring_records)} |
| Score Threshold | {self.metrics.threshold} |
| Timestamp | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |
| Metric | Value | +
|---|---|
| Mean Score | {self.metrics.mean_score:.4f} |
| Median Score | {self.metrics.median_score:.4f} |
| Standard Deviation | {self.metrics.std_dev:.4f} |
| Min Score | {self.metrics.min_score:.4f} |
| Max Score | {self.metrics.max_score:.4f} |
| Metric | Value | Status | +
|---|---|---|
| Accuracy | +{self.metrics.accuracy:.1%} | ++ {'✅' if self.metrics.accuracy >= 0.70 else '❌'} {'Pass' if self.metrics.accuracy >= 0.70 else 'Fail'} (target: ≥ 0.70) + | +
| Precision | +{self.metrics.precision:.1%} | ++ {'✅' if self.metrics.precision >= 0.65 else '❌'} {'Pass' if self.metrics.precision >= 0.65 else 'Fail'} (target: ≥ 0.65) + | +
| Recall | +{self.metrics.recall:.1%} | ++ {'✅' if self.metrics.recall >= 0.65 else '❌'} {'Pass' if self.metrics.recall >= 0.65 else 'Fail'} (target: ≥ 0.65) + | +
| F1 Score | +{self.metrics.f1:.4f} | ++ |
| ROC-AUC | +{self.metrics.roc_auc:.4f} | ++ |
| Predicted Relevant | Predicted Irrelevant | |
|---|---|---|
| Actually Relevant | {self.metrics.true_positives} | {self.metrics.false_negatives} |
| Actually Irrelevant | {self.metrics.false_positives} | {self.metrics.true_negatives} |
| Metric | Value | Status | +
|---|---|---|
| P50 Latency | +{self.metrics.p50_latency_ms:.1f} ms | ++ |
| P95 Latency | +{self.metrics.p95_latency_ms:.1f} ms | ++ {'✅' if self.metrics.p95_latency_ms < 500 else '❌'} {'Pass' if self.metrics.p95_latency_ms < 500 else 'Fail'} (target: < 500ms) + | +
| P99 Latency | +{self.metrics.p99_latency_ms:.1f} ms | ++ |
| Total Runtime | +{self.metrics.total_runtime_sec:.1f} sec | ++ |
| Throughput | +{self.metrics.throughput_per_sec:.1f} samples/sec | ++ |
| Metric | Value | +
|---|---|
| Score-to-Label Correlation | +{self.metrics.correlation_score_to_label:.4f} | +
| NULL/NaN Scores | +{self.metrics.null_score_count} ({self.metrics.null_score_count / len(self.scoring_records):.1%}) | +
| Short Reviews (<5 words) | +{self.metrics.short_review_count} | +
| Long Reviews (>300 words) | +{self.metrics.long_review_count} | +
| Poor Book Metadata (<5 words) | +{self.metrics.poor_metadata_count} | +
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+