From ec601c3e6d593004cff2612d3de5eba20c4689ba Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Sun, 17 Sep 2023 10:40:10 -0400 Subject: [PATCH] Updates for moving acme-assist fully on Spring AI * Add SimplePersistentVectorStore --- .../springframework/ai/document/Document.java | 49 +++++----- .../impl/EmptyJsonMetadataGenerator.java | 15 +++ .../ai/loader/impl/JsonLoader.java | 43 ++++++--- .../ai/loader/impl/JsonMetadataGenerator.java | 14 +++ .../ai/splitter/TextSplitter.java | 2 +- .../vectorstore/impl/InMemoryVectorStore.java | 9 +- .../impl/SimplePersistentVectorStore.java | 96 +++++++++++++++++++ .../ai/openai/OpenAiTestConfiguration.java | 5 +- .../SimplePersistentVectorStoreIT.java | 62 ++++++++++++ .../src/test/resources/data/acme/bikes.json | 81 ++++++++++++++++ 10 files changed, 329 insertions(+), 47 deletions(-) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/loader/impl/EmptyJsonMetadataGenerator.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonMetadataGenerator.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/SimplePersistentVectorStore.java create mode 100644 spring-ai-openai/src/test/java/org/springframework/ai/openai/vectorstore/SimplePersistentVectorStoreIT.java create mode 100644 spring-ai-openai/src/test/resources/data/acme/bikes.json diff --git a/spring-ai-core/src/main/java/org/springframework/ai/document/Document.java b/spring-ai-core/src/main/java/org/springframework/ai/document/Document.java index ce1e89f3c..896b9ab69 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/document/Document.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/document/Document.java @@ -1,5 +1,8 @@ package org.springframework.ai.document; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.util.StringUtils; import java.util.*; @@ -11,26 +14,22 @@ public class Document { */ private final String id; + @JsonProperty(index = 100) private List embedding = new ArrayList<>(); /** * Metadata for the document. It should not be nested and values should be restricted * to string, int, float, boolean for simple use with Vector Dbs. */ - private Map metadata = new HashMap<>(); + private Map metadata; // Type; introduce when support images, now only text. - // TODO: Rename to `content` instead. private String text; - public Document(String text) { - this(UUID.randomUUID().toString(), text); - } - - public Document(String id, String text) { - this.id = id; - this.text = text; + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Document(@JsonProperty("text") String text) { + this(text, new HashMap<>()); } public Document(String text, Map metadata) { @@ -68,26 +67,21 @@ public class Document { return "Document{" + "id='" + id + '\'' + ", metadata=" + metadata + ", text='" + text + '\'' + '}'; } - // TODO: Consider moving the following methods & fields in a seprarate - // dedicated class. (e.g. DocumentService, DocumentUtil or alike)¬ - - // private List excludedMetadataKeysForEmbedding; - // private List relatedIds; - private static String DEFAULT_TEXT_TEMPLATE = "{metadata_string}\n\n{text}"; private static String DEFAULT_METADATA_TEMPLATE = "{key}: {value}"; - private final String textTemplate = DEFAULT_TEXT_TEMPLATE; + private String textTemplate = DEFAULT_TEXT_TEMPLATE; - private final String metadataTemplate = DEFAULT_METADATA_TEMPLATE; + private String metadataTemplate = DEFAULT_METADATA_TEMPLATE; - private final String metadataSeparator = "\n"; + private String metadataSeparator = "\n"; private MetadataMode metadataMode = MetadataMode.NONE; private List excludedMetadataKeysForLlm; + @JsonIgnore public String getContent() { return getContent(MetadataMode.ALL); } @@ -103,6 +97,7 @@ public class Document { return getTextTemplate().replace("{metadata_string}", metadataString).replace("{text}", text); } + @JsonIgnore public String getMetadataString() { return getMetadataString(metadataMode); } @@ -144,16 +139,16 @@ public class Document { return metadataSeparator; } - // public void setTextTemplate(String textTemplate) { - // this.textTemplate = textTemplate; - // } + public void setTextTemplate(String textTemplate) { + this.textTemplate = textTemplate; + } - // public void setMetadataTemplate(String metadataTemplate) { - // this.metadataTemplate = metadataTemplate; - // } + public void setMetadataTemplate(String metadataTemplate) { + this.metadataTemplate = metadataTemplate; + } - // public void setMetadataSeparator(String metadataSeparator) { - // this.metadataSeparator = metadataSeparator; - // } + public void setMetadataSeparator(String metadataSeparator) { + this.metadataSeparator = metadataSeparator; + } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/EmptyJsonMetadataGenerator.java b/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/EmptyJsonMetadataGenerator.java new file mode 100644 index 000000000..948f53895 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/EmptyJsonMetadataGenerator.java @@ -0,0 +1,15 @@ +package org.springframework.ai.loader.impl; + +import java.util.Collections; +import java.util.Map; + +public class EmptyJsonMetadataGenerator implements JsonMetadataGenerator { + + private static final Map EMPTY_MAP = Collections.emptyMap(); + + @Override + public Map generate(Map jsonMap) { + return EMPTY_MAP; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonLoader.java b/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonLoader.java index ec6152dfd..48ad2b4ea 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonLoader.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonLoader.java @@ -13,23 +13,30 @@ import java.util.*; public class JsonLoader implements Loader { + private Resource resource; + + private JsonMetadataGenerator jsonMetadataGenerator; + /** * The key from the JSON that we will use as the text to parse into the Document text */ - private List jsonKeysToUse = new ArrayList<>(); - - private Resource resource; + private List jsonKeysToUse; public JsonLoader(Resource resource) { - Objects.requireNonNull(resource, "The Spring Resource must not be null"); - this.resource = resource; + this(resource, new ArrayList<>().toArray(new String[0])); } public JsonLoader(Resource resource, String... jsonKeysToUse) { + this(resource, new EmptyJsonMetadataGenerator(), jsonKeysToUse); + } + + public JsonLoader(Resource resource, JsonMetadataGenerator jsonMetadataGenerator, String... jsonKeysToUse) { Objects.requireNonNull(jsonKeysToUse, "keys must not be null"); + Objects.requireNonNull(jsonMetadataGenerator, "jsonMetadataGenerator must not be null"); Objects.requireNonNull(resource, "The Spring Resource must not be null"); - this.jsonKeysToUse = List.of(jsonKeysToUse); this.resource = resource; + this.jsonMetadataGenerator = jsonMetadataGenerator; + this.jsonKeysToUse = List.of(jsonKeysToUse); } @Override @@ -41,9 +48,6 @@ public class JsonLoader implements Loader { public List load(TextSplitter textSplitter) { ObjectMapper objectMapper = new ObjectMapper(); - TypeReference> typeRef = new TypeReference>() { - }; - List documents = new ArrayList<>(); try { // TODO, not all json will be an array @@ -61,21 +65,30 @@ public class JsonLoader implements Loader { } } + Map metadata = this.jsonMetadataGenerator.generate(item); + + Document document; if (!sb.isEmpty()) { - Document document = new Document(sb.toString()); - documents.add(document); + document = new Document(sb.toString(), metadata); } else { - Document document = new Document(item.toString()); - documents.add(document); + document = new Document(item.toString(), metadata); } + // Splitting at the item level is good when the size of the json per + // element is large + // as is the case with a catalog of product, as the metadata applies + // across all split documents + // This may not be good when the size of the json element is small as it + // can create too many individual + // documents. + List splitDocuments = textSplitter.apply(List.of(document)); + documents.addAll(splitDocuments); } } catch (IOException e) { throw new RuntimeException(e); } - List splitDocuments = textSplitter.apply(documents); - return splitDocuments; + return documents; } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonMetadataGenerator.java b/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonMetadataGenerator.java new file mode 100644 index 000000000..61774f6e6 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/loader/impl/JsonMetadataGenerator.java @@ -0,0 +1,14 @@ +package org.springframework.ai.loader.impl; + +import java.util.Map; + +@FunctionalInterface +public interface JsonMetadataGenerator { + + /** + * The input is the JSON document represented as a map, the output are the fields + * extracted from the input map that will be used as metadata. + */ + Map generate(Map jsonMap); + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/splitter/TextSplitter.java b/spring-ai-core/src/main/java/org/springframework/ai/splitter/TextSplitter.java index c9ff10ac5..c955f5ff5 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/splitter/TextSplitter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/splitter/TextSplitter.java @@ -41,7 +41,7 @@ public abstract class TextSplitter implements DocumentTransformer { String text = texts.get(i); List chunks = splitText(text); if (chunks.size() > 1) { - logger.info("Broke up document " + i + " into " + chunks.size() + " chunks."); + logger.info("Splitting up document into " + chunks.size() + " chunks."); } for (String chunk : chunks) { // only primitive values are in here - diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/InMemoryVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/InMemoryVectorStore.java index 47a25d9c1..e7852aea9 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/InMemoryVectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/InMemoryVectorStore.java @@ -1,5 +1,7 @@ package org.springframework.ai.vectorstore.impl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingClient; import org.springframework.ai.vectorstore.VectorStore; @@ -14,9 +16,11 @@ import java.util.concurrent.ConcurrentHashMap; */ public class InMemoryVectorStore implements VectorStore { - private Map store = new ConcurrentHashMap<>(); + private static final Logger logger = LoggerFactory.getLogger(InMemoryVectorStore.class); - private EmbeddingClient embeddingClient; + protected Map store = new ConcurrentHashMap<>(); + + protected EmbeddingClient embeddingClient; public InMemoryVectorStore(EmbeddingClient embeddingClient) { Objects.requireNonNull(embeddingClient, "EmbeddingClient must not be null"); @@ -26,6 +30,7 @@ public class InMemoryVectorStore implements VectorStore { @Override public void add(List documents) { for (Document document : documents) { + logger.info("Calling EmbeddingClient for document id = " + document.getId()); List embedding = this.embeddingClient.embed(document); document.setEmbedding(embedding); this.store.put(document.getId(), document); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/SimplePersistentVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/SimplePersistentVectorStore.java new file mode 100644 index 000000000..bfd70363f --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/impl/SimplePersistentVectorStore.java @@ -0,0 +1,96 @@ +package org.springframework.ai.vectorstore.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.core.io.Resource; +import org.springframework.util.StreamUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * Adds simple serialization/deserialization to the data stored in the InMemoryVectorStore + */ +public class SimplePersistentVectorStore extends InMemoryVectorStore { + + private static final Logger logger = LoggerFactory.getLogger(SimplePersistentVectorStore.class); + + public SimplePersistentVectorStore(EmbeddingClient embeddingClient) { + super(embeddingClient); + } + + public void save(File file) { + String json = getVectorDbAsJson(); + try { + if (!file.exists()) { + logger.info("Creating new vector store file: " + file); + file.createNewFile(); + } + else { + logger.info("Replacing existing vector store file: " + file); + file.delete(); + file.createNewFile(); + } + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + try (OutputStream stream = new FileOutputStream(file)) { + StreamUtils.copy(json, Charset.forName("UTF-8"), stream); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void load(File file) { + TypeReference> typeRef = new TypeReference<>() { + }; + ObjectMapper objectMapper = new ObjectMapper(); + try { + Map deserializedMap = objectMapper.readValue(file, typeRef); + this.store = deserializedMap; + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public void load(Resource resource) { + TypeReference> typeRef = new TypeReference<>() { + }; + ObjectMapper objectMapper = new ObjectMapper(); + try { + Map deserializedMap = objectMapper.readValue(resource.getInputStream(), typeRef); + this.store = deserializedMap; + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private String getVectorDbAsJson() { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); + String json; + try { + json = objectWriter.writeValueAsString(this.store); + } + catch (JsonProcessingException e) { + throw new RuntimeException("Error serializing documentMap to JSON.", e); + } + return json; + } + +} diff --git a/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java b/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java index 85c5dd301..f92591b5d 100644 --- a/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java +++ b/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java @@ -1,8 +1,9 @@ package org.springframework.ai.openai; import com.theokanning.openai.service.OpenAiService; -import org.springframework.ai.openai.embedding.OpenAiEmbeddingClient; +import org.springframework.ai.embedding.EmbeddingClient; import org.springframework.ai.openai.client.OpenAiClient; +import org.springframework.ai.openai.embedding.OpenAiEmbeddingClient; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.util.StringUtils; @@ -32,7 +33,7 @@ public class OpenAiTestConfiguration { } @Bean - public OpenAiEmbeddingClient openAiEmbeddingClient(OpenAiService theoOpenAiService) { + public EmbeddingClient openAiEmbeddingClient(OpenAiService theoOpenAiService) { return new OpenAiEmbeddingClient(theoOpenAiService); } diff --git a/spring-ai-openai/src/test/java/org/springframework/ai/openai/vectorstore/SimplePersistentVectorStoreIT.java b/spring-ai-openai/src/test/java/org/springframework/ai/openai/vectorstore/SimplePersistentVectorStoreIT.java new file mode 100644 index 000000000..350836e3d --- /dev/null +++ b/spring-ai-openai/src/test/java/org/springframework/ai/openai/vectorstore/SimplePersistentVectorStoreIT.java @@ -0,0 +1,62 @@ +package org.springframework.ai.openai.vectorstore; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.loader.impl.JsonLoader; +import org.springframework.ai.loader.impl.JsonMetadataGenerator; +import org.springframework.ai.vectorstore.impl.SimplePersistentVectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.Resource; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class SimplePersistentVectorStoreIT { + + @Value("classpath:/data/acme/bikes.json") + private Resource bikesJsonResource; + + @Autowired + private EmbeddingClient embeddingClient; + + @Test + void persist(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path workingDir) { + JsonLoader jsonLoader = new JsonLoader(bikesJsonResource, new ProductMetadataGenerator(), "price", "name", + "shortDescription", "description", "tags"); + List documents = jsonLoader.load(); + SimplePersistentVectorStore vectorStore = new SimplePersistentVectorStore(this.embeddingClient); + vectorStore.add(documents); + + File tempFile = new File(workingDir.toFile(), "temp.txt"); + vectorStore.save(tempFile); + assertThat(tempFile).isNotEmpty(); + assertThat(tempFile).content().contains("Velo 99 XR1 AXS"); + SimplePersistentVectorStore vectorStore2 = new SimplePersistentVectorStore(this.embeddingClient); + + vectorStore2.load(tempFile); + List similaritySearch = vectorStore2.similaritySearch("Velo 99 XR1 AXS"); + assertThat(similaritySearch).isNotEmpty(); + assertThat(similaritySearch.get(0).getMetadata()).containsEntry("name", "Velo 99 XR1 AXS"); + + } + + public class ProductMetadataGenerator implements JsonMetadataGenerator { + + @Override + public Map generate(Map jsonMap) { + return Map.of("name", jsonMap.get("name")); + } + + } + +} diff --git a/spring-ai-openai/src/test/resources/data/acme/bikes.json b/spring-ai-openai/src/test/resources/data/acme/bikes.json new file mode 100644 index 000000000..a7c2f401f --- /dev/null +++ b/spring-ai-openai/src/test/resources/data/acme/bikes.json @@ -0,0 +1,81 @@ +[ + { + "name": "E-Adrenaline 8.0 EX1", + "shortDescription": "a versatile and comfortable e-MTB designed for adrenaline enthusiasts who want to explore all types of terrain. It features a powerful motor and advanced suspension to provide a smooth and responsive ride, with a variety of customizable settings to fit any rider's needs.", + "description": "## Overview\r\nIt's right for you if...\r\nYou want to push your limits on challenging trails and terrain, with the added benefit of an electric assist to help you conquer steep climbs and rough terrain. You also want a bike with a comfortable and customizable fit, loaded with high-quality components and technology.\r\n\r\nThe tech you get\r\nA lightweight, full ADV Mountain Carbon frame with a customizable geometry, including an adjustable head tube and chainstay length. A powerful and efficient motor with a 375Wh battery that can assist up to 28 mph when it's on, and provides a smooth and seamless transition when it's off. A SRAM EX1 8-speed drivetrain, a RockShox Lyrik Ultimate fork, and a RockShox Super Deluxe Ultimate rear shock.\r\n\r\nThe final word\r\nOur E-Adrenaline 8.0 EX1 is the perfect bike for adrenaline enthusiasts who want to explore all types of terrain. It's versatile, comfortable, and loaded with advanced technology to provide a smooth and responsive ride, no matter where your adventures take you.\r\n\r\n\r\n## Features\r\nVersatile and customizable\r\nThe E-Adrenaline 8.0 EX1 features a customizable geometry, including an adjustable head tube and chainstay length, so you can fine-tune your ride to fit your needs and preferences. It also features a variety of customizable settings, including suspension tuning, motor assistance levels, and more.\r\n\r\nPowerful and efficient\r\nThe bike is equipped with a powerful and efficient motor that provides a smooth and seamless transition between human power and electric assist. It can assist up to 28 mph when it's on, and provides zero drag when it's off.\r\n\r\nAdvanced suspension\r\nThe E-Adrenaline 8.0 EX1 features a RockShox Lyrik Ultimate fork and a RockShox Super Deluxe Ultimate rear shock, providing advanced suspension technology to absorb shocks and bumps on any terrain. The suspension is also customizable to fit your riding style and preferences.\r\n\r\n\r\n## Specs\r\nFrameset\r\nFrame ADV Mountain Carbon main frame & stays, adjustable head tube and chainstay length, tapered head tube, Knock Block, Control Freak internal routing, Boost148, 150mm travel\r\nFork RockShox Lyrik Ultimate, DebonAir spring, Charger 2.1 RC2 damper, remote lockout, tapered steerer, 42mm offset, Boost110, 15mm Maxle Stealth, 160mm travel\r\nShock RockShox Super Deluxe Ultimate, DebonAir spring, Thru Shaft 3-position damper, 230x57.5mm\r\n\r\nWheels\r\nWheel front Bontrager Line Elite 30, ADV Mountain Carbon, Tubeless Ready, 6-bolt, Boost110, 15mm thru axle\r\nWheel rear Bontrager Line Elite 30, ADV Mountain Carbon, Tubeless Ready, 54T Rapid Drive, 6-bolt, Shimano MicroSpline freehub, Boost148, 12mm thru axle\r\nSkewer rear Bontrager Switch thru axle, removable lever\r\nTire Bontrager XR5 Team Issue, Tubeless Ready, Inner Strength sidewall, aramid bead, 120tpi, 29x2.50''\r\nTire part Bontrager TLR sealant, 6oz\r\n\r\nDrivetrain\r\nShifter SRAM EX1, 8 speed\r\nRear derailleur SRAM EX1, 8 speed\r\nCrank Bosch Performance CX, magnesium motor body, 250 watt, 75 Nm torque\r\nChainring SRAM EX1, 18T, steel\r\nCassette SRAM EX1, 11-48, 8 speed\r\nChain SRAM EX1, 8 speed\r\n\r\nComponents\r\nSaddle Bontrager Arvada, hollow chromoly rails, 138mm width\r\nSeatpost Bontrager Line Elite Dropper, internal routing, 31.6mm\r\nHandlebar Bontrager Line Pro, ADV Carbon, 35mm, 27.5mm rise, 780mm width\r\nGrips Bontrager XR Trail Elite, alloy lock-on\r\nStem Bontrager Line Pro, 35mm, Knock Block, Blendr compatible, 0 degree, 50mm length\r\nHeadset Knock Block Integrated, 62-degree radius, cartridge bearing, 1-1\/8'' top, 1.5'' bottom\r\nBrake SRAM G2 RSC hydraulic disc, carbon levers\r\nBrake rotor SRAM Centerline, centerlock, round edge, 200mm\r\n\r\nAccessories\r\nE-bike system Bosch Performance CX, magnesium motor body, 250 watt, 75 Nm torque\r\nBattery Bosch PowerTube 625, 625Wh\r\nCharger Bosch 4A standard charger\r\nController Bosch Kiox with Anti-theft solution, Bluetooth connectivity, 1.9'' display\r\nTool Bontrager Switch thru axle, removable lever\r\n\r\nWeight\r\nWeight M - 20.25 kg \/ 44.6 lbs (with TLR sealant, no tubes)\r\nWeight limit This bike has a maximum total weight limit (combined weight of bicycle, rider, and cargo) of 300 pounds (136 kg).\r\n\r\n## Sizing & fit\r\n\r\n| Size | Rider Height | Inseam |\r\n|:----:|:------------------------:|:--------------------:|\r\n| S | 155 - 170 cm 5'1\" - 5'7\" | 73 - 80 cm 29\" - 31.5\" |\r\n| M | 163 - 178 cm 5'4\" - 5'10\" | 77 - 83 cm 30.5\" - 32.5\" |\r\n| L | 176 - 191 cm 5'9\" - 6'3\" | 83 - 89 cm 32.5\" - 35\" |\r\n| XL | 188 - 198 cm 6'2\" - 6'6\" | 88 - 93 cm 34.5\" - 36.5\" |\r\n\r\n\r\n## Geometry\r\n\r\nAll measurements provided in cm unless otherwise noted.\r\nSizing table\r\n| Frame size letter | S | M | L | XL |\r\n|---------------------------|-------|-------|-------|-------|\r\n| Actual frame size | 15.8 | 17.8 | 19.8 | 21.8 |\r\n| Wheel size | 29\" | 29\" | 29\" | 29\" |\r\n| A \u2014 Seat tube | 40.0 | 42.5 | 47.5 | 51.0 |\r\n| B \u2014 Seat tube angle | 72.5\u00B0 | 72.8\u00B0 | 73.0\u00B0 | 73.0\u00B0 |\r\n| C \u2014 Head tube length | 9.5 | 10.5 | 11.0 | 11.5 |\r\n| D \u2014 Head angle | 67.8\u00B0 | 67.8\u00B0 | 67.8\u00B0 | 67.8\u00B0 |\r\n| E \u2014 Effective top tube | 59.0 | 62.0 | 65.0 | 68.0 |\r\n| F \u2014 Bottom bracket height | 32.5 | 32.5 | 32.5 | 32.5 |\r\n| G \u2014 Bottom bracket drop | 5.5 | 5.5 | 5.5 | 5.5 |\r\n| H \u2014 Chainstay length | 45.0 | 45.0 | 45.0 | 45.0 |\r\n| I \u2014 Offset | 4.5 | 4.5 | 4.5 | 4.5 |\r\n| J \u2014 Trail | 11.0 | 11.0 | 11.0 | 11.0 |\r\n| K \u2014 Wheelbase | 113.0 | 117.0 | 120.0 | 123.0 |\r\n| L \u2014 Standover | 77.0 | 77.0 | 77.0 | 77.0 |\r\n| M \u2014 Frame reach | 41.0 | 44.5 | 47.5 | 50.0 |\r\n| N \u2014 Frame stack | 61.0 | 62.0 | 62.5 | 63.0 |", + "price": 1499.99, + "tags": [ + "bicycle" + ] + }, + { + "name": "Enduro X Pro", + "shortDescription": "The Enduro X Pro is the ultimate mountain bike for riders who demand the best. With its full carbon frame and top-of-the-line components, this bike is ready to tackle any trail, from technical downhill descents to grueling uphill climbs.", + "description": "## Overview\nIt's right for you if...\nYou're an experienced mountain biker who wants a high-performance bike that can handle any terrain. You want a bike with the best components available, including a full carbon frame, suspension system, and hydraulic disc brakes.\n\nThe tech you get\nOur top-of-the-line full carbon frame with aggressive geometry and a slack head angle for maximum control. It's equipped with a Fox Factory suspension system with 170mm of travel in the front and 160mm in the rear, a Shimano XTR 12-speed drivetrain, and hydraulic disc brakes for maximum stopping power. The bike also features a dropper seatpost for easy adjustments on the fly.\n\nThe final word\nThe Enduro X Pro is the ultimate mountain bike for riders who demand the best. With its full carbon frame, top-of-the-line components, and aggressive geometry, this bike is ready to take on any trail. Whether you're a seasoned pro or just starting out, the Enduro X Pro will help you take your riding to the next level.\n\n## Features\nFull carbon frame\nAggressive geometry with a slack head angle\nFox Factory suspension system with 170mm of travel in the front and 160mm in the rear\nShimano XTR 12-speed drivetrain\nHydraulic disc brakes for maximum stopping power\nDropper seatpost for easy adjustments on the fly\n\n## Specifications\nFrameset\nFrame\tFull carbon frame\nFork\tFox Factory suspension system with 170mm of travel\nRear suspension\tFox Factory suspension system with 160mm of travel\n\nWheels\nWheel size\t27.5\" or 29\"\nTires\tTubeless-ready Maxxis tires\n\nDrivetrain\nShifters\tShimano XTR 12-speed\nFront derailleur\tN/A\nRear derailleur\tShimano XTR\nCrankset\tShimano XTR\nCassette\tShimano XTR 12-speed\nChain\tShimano XTR\n\nComponents\nBrakes\tHydraulic disc brakes\nHandlebar\tAlloy handlebar\nStem\tAlloy stem\nSeatpost\tDropper seatpost\n\nAccessories\nPedals\tNot included\n\nWeight\nWeight\tApproximately 27-29 lbs\n\n## Sizing\n| Size | Rider Height |\n|:----:|:-------------------------:|\n| S | 5'4\" - 5'8\" (162-172cm) |\n| M | 5'8\" - 5'11\" (172-180cm) |\n| L | 5'11\" - 6'3\" (180-191cm) |\n| XL | 6'3\" - 6'6\" (191-198cm) |\n\n## Geometry\n| Size | S | M | L | XL |\n|:----:|:---------------:|:---------------:|:-----------------:|:---------------:|\n| A - Seat tube length | 390mm | 425mm | 460mm | 495mm |\n| B - Effective top tube length | 585mm | 610mm | 635mm | 660mm |\n| C - Head tube angle | 65.5° | 65.5° | 65.5° | 65.5° |\n| D - Seat tube angle | 76° | 76° | 76° | 76° |\n| E - Chainstay length | 435mm | 435mm | 435mm | 435mm |\n| F - Head tube length | 100mm | 110mm | 120mm | 130mm |\n| G - BB drop | 20mm | 20mm | 20mm | 20mm |\n| H - Wheelbase | 1155mm | 1180mm | 1205mm | 1230mm |\n| I - Standover height | 780mm | 800mm | 820mm | 840mm |\n| J - Reach | 425mm | 450mm | 475mm | 500mm |\n| K - Stack | 610mm | 620mm | 630mm | 640mm |", + "price": 599.99, + "tags": [ + "bicycle" + ] + }, + { + "name": "Blaze X1", + "shortDescription": "Blaze X1 is a high-performance road bike that offers superior speed and agility, making it perfect for competitive racing or fast-paced group rides. The bike features a lightweight carbon frame, aerodynamic tube shapes, a 12-speed Shimano Ultegra drivetrain, and hydraulic disc brakes for precise stopping power. With its sleek design and cutting-edge technology, Blaze X1 is a bike that is built to perform and dominate on any road.", + "description": "## Overview\nIt's right for you if...\nYou're a competitive road cyclist or an enthusiast who enjoys fast-paced group rides. You want a bike that is lightweight, agile, and delivers exceptional speed.\n\nThe tech you get\nBlaze X1 features a lightweight carbon frame with a tapered head tube and aerodynamic tube shapes for maximum speed and efficiency. The bike is equipped with a 12-speed Shimano Ultegra drivetrain for smooth and precise shifting, Shimano hydraulic disc brakes for powerful and reliable stopping power, and Bontrager Aeolus Elite 35 carbon wheels for increased speed and agility.\n\nThe final word\nBlaze X1 is a high-performance road bike that is designed to deliver exceptional speed and agility. With its cutting-edge technology and top-of-the-line components, it's a bike that is built to perform and dominate on any road.\n\n## Features\nSpeed and efficiency\nBlaze X1's lightweight carbon frame and aerodynamic tube shapes offer maximum speed and efficiency, allowing you to ride faster and farther with ease.\n\nPrecision stopping power\nShimano hydraulic disc brakes provide precise and reliable stopping power, even in wet or muddy conditions.\n\nAgility and control\nBontrager Aeolus Elite 35 carbon wheels make Blaze X1 incredibly agile and responsive, allowing you to navigate tight turns and corners with ease.\n\nSmooth and precise shifting\nThe 12-speed Shimano Ultegra drivetrain offers smooth and precise shifting, so you can easily find the right gear for any terrain.\n\n## Specifications\nFrameset\nFrame\tADV Carbon, tapered head tube, BB90, direct mount rim brakes, internal cable routing, DuoTrap S compatible, 130x9mm QR\nFork\tADV Carbon, tapered steerer, direct mount rim brakes, internal brake routing, 100x9mm QR\n\nWheels\nWheel front\tBontrager Aeolus Elite 35, ADV Carbon, Tubeless Ready, 35mm rim depth, 100x9mm QR\nWheel rear\tBontrager Aeolus Elite 35, ADV Carbon, Tubeless Ready, 35mm rim depth, Shimano 11-speed freehub, 130x9mm QR\nTire front\tBontrager R3 Hard-Case Lite, aramid bead, 120 tpi, 700x25c\nTire rear\tBontrager R3 Hard-Case Lite, aramid bead, 120 tpi, 700x25c\nMax tire size\t25c Bontrager tires (with at least 4mm of clearance to frame)\n\nDrivetrain\nShifter\tShimano Ultegra R8020, 12 speed\nFront derailleur\tShimano Ultegra R8000, braze-on\nRear derailleur\tShimano Ultegra R8000, short cage, 30T max cog\nCrank\tSize: 50, 52, 54\nShimano Ultegra R8000, 50/34 (compact), 170mm length\nSize: 56, 58, 60, 62\nShimano Ultegra R8000, 50/34 (compact), 172.5mm length\nBottom bracket\tBB90, Shimano press-fit\nCassette\tShimano Ultegra R8000, 11-30, 12 speed\nChain\tShimano Ultegra HG701, 12 speed\n\nComponents\nSaddle\tBontrager Montrose Elite, titanium rails, 138mm width\nSeatpost\tBontrager carbon seatmast cap, 20mm offset\nHandlebar\tBontrager Elite Aero VR-CF, alloy, 31.8mm, internal cable routing, 40cm width\nGrips\tBontrager Supertack Perf tape\nStem\tBontrager Elite, 31.8mm, Blendr-compatible, 7 degree, 80mm length\nBrake Shimano Ultegra hydraulic disc brake\n\nWeight\nWeight\t56 - 8.91 kg / 19.63 lbs (with tubes)\nWeight limit\tThis bike has a maximum total weight limit (combined weight of bicycle, rider, and cargo) of 275 pounds (125 kg).\n\n## Sizing\n| Size | Rider height |\n|------|-------------|\n| 50 | 162-166cm |\n| 52 | 165-170cm |\n| 54 | 168-174cm |\n| 56 | 174-180cm |\n| 58 | 179-184cm |\n| 60 | 184-189cm |\n| 62 | 189-196cm |\n\n## Geometry\n| Frame size | 50cm | 52cm | 54cm | 56cm | 58cm | 60cm | 62cm |\n|------------|-------|-------|-------|-------|-------|-------|-------|\n| Wheel size | 700c | 700c | 700c | 700c | 700c | 700c | 700c |\n| A - Seat tube | 443mm | 460mm | 478mm | 500mm | 520mm | 540mm | 560mm |\n| B - Seat tube angle | 74.1° | 73.9° | 73.7° | 73.4° | 73.2° | 73.0° | 72.8° |\n| C - Head tube length | 100mm | 110mm | 130mm | 150mm | 170mm | 190mm | 210mm |\n| D - Head angle | 71.4° | 72.0° | 72.5° | 73.0° | 73.3° | 73.6° | 73.8° |\n| E - Effective top tube | 522mm | 535mm | 547mm | 562mm | 577mm | 593mm | 610mm |\n| F - Bottom bracket height | 268mm | 268mm | 268mm | 268mm | 268mm | 268mm | 268mm |\n| G - Bottom bracket drop | 69mm | 69mm | 69mm | 69mm | 69mm | 69mm | 69mm |\n| H - Chainstay length | 410mm | 410mm | 410mm | 410mm | 410mm | 410mm | 410mm |\n| I - Offset | 50mm | 50mm | 50mm | 50mm | 50mm | 50mm | 50mm |\n| J - Trail | 65mm | 62mm | 59mm | 56mm | 55mm | 53mm | 52mm |\n| K - Wheelbase | 983mm | 983mm | 990mm | 1005mm | 1019mm | 1036mm | 1055mm |\n| L - Standover | 741mm | 765mm | 787mm | 806mm | 825mm | 847mm | 869mm |", + "price": 799.99, + "tags": [ + "bicycle", + "mountain bike" + ] + }, + { + "name": "Celerity X5", + "shortDescription": "Celerity X5 is a versatile and reliable road bike that is designed for experienced and amateur riders alike. It's designed to provide smooth and comfortable rides over long distances. With an ultra-lightweight and responsive carbon fiber frame, Shimano 105 groupset, hydraulic disc brakes, and 28mm wide tires, this bike ensures efficient power transfer, precise handling, and superior stopping power.", + "description": "## Overview\n\nIt's right for you if... \nYou are looking for a high-performance road bike that offers a perfect balance of speed, comfort, and control. You enjoy long-distance rides and need a bike that is designed to handle various road conditions with ease. You also appreciate the latest technology and reliable components that make your riding experience more enjoyable.\n\nThe tech you get \nCelerity X5 is equipped with a full carbon fiber frame that ensures maximum strength and durability while keeping the weight down. It features a Shimano 105 groupset with 11-speed gearing for precise and efficient shifting. Hydraulic disc brakes offer superior stopping power, and 28mm wide tires provide comfort and stability on various road surfaces. Internal cable routing enhances the bike's sleek appearance.\n\nThe final word \nIf you are looking for a high-performance road bike that offers comfort, speed, and control, Celerity X5 is the perfect choice. With its lightweight carbon fiber frame, reliable components, and advanced technology, this bike is designed to help you enjoy long-distance rides with ease.\n\n## Features \n\nLightweight and responsive \nCelerity X5 comes with a full carbon fiber frame that is not only lightweight but also responsive, providing excellent handling and control.\n\nHydraulic disc brakes \nThis bike is equipped with hydraulic disc brakes that provide superior stopping power in all weather conditions, ensuring your safety and confidence on the road.\n\nComfortable rides \nThe 28mm wide tires and carbon seat post provide ample cushioning, ensuring a smooth and comfortable ride over long distances.\n\nSleek appearance \nThe bike's internal cable routing enhances its sleek appearance while also protecting the cables from the elements, ensuring smooth shifting for longer periods.\n\n## Specifications \n\nFrameset \nFrame\tCelerity X5 Full Carbon Fiber Frame, Internal Cable Routing, Tapered Headtube, Press Fit Bottom Bracket, 12x142mm Thru-Axle \nFork\tCelerity X5 Full Carbon Fiber Fork, Internal Brake Routing, 12x100mm Thru-Axle \n\nWheels \nWheelset\tAlexRims CXD7 Wheelset \nTire\tSchwalbe Durano Plus 700x28mm \nInner Tubes\tSchwalbe SV15 700x18-28mm \nSkewers\tCelerity X5 Thru-Axle Skewers \n\nDrivetrain \nShifter\tShimano 105 R7025 Hydraulic Disc Shifters \nFront Derailleur\tShimano 105 R7000 \nRear Derailleur\tShimano 105 R7000 \nCrankset\tShimano 105 R7000 50-34T \nBottom Bracket\tShimano BB72-41B \nCassette\tShimano 105 R7000 11-30T \nChain\tShimano HG601 11-Speed Chain \n\nComponents \nSaddle\tSelle Royal Asphalt Saddle \nSeatpost\tCelerity X5 Carbon Seatpost \nHandlebar\tCelerity X5 Compact Handlebar \nStem\tCelerity X5 Aluminum Stem \nHeadset\tFSA Orbit IS-2 \n\nBrakes \nBrakes\tShimano 105 R7025 Hydraulic Disc Brakes \nRotors\tShimano SM-RT70 160mm Rotors \n\nAccessories \nPedals\tCelerity X5 Road Pedals \n\nWeight \nWeight\t8.2 kg / 18.1 lbs \nWeight Limit\tThis bike has a maximum total weight limit (combined weight of bicycle, rider, and cargo) of 120 kg (265 lbs).\n\n## Sizing \n| Size | Rider Height | Inseam |\n|:----:|:-------------------------:|:--------------------:|\n| 49 | 155 - 162 cm 5'1\" - 5'4\" | 71 - 76 cm 28\" - 30\" |\n| 52 | 162 - 170 cm 5'4\" - 5'7\" | 74 - 79 cm 29\" - 31\" |\n| 54 | 170 - 178 cm 5'7\" - 5'10\" | 77 - 83 cm 30\" - 32\" |\n| 56 | 178 - 185 cm 5'10\" - 6'1\" | 82 - 88 cm 32\" - 34\" |\n| 58 | 185 - 193 cm 6'1\" - 6'4\" | 86 - 92 cm 34\" - 36\" |\n| 61 | 193 - 200 cm 6'4\" - 6'7\" | 90 - 95 cm 35\" - 37\" |\n\n## Geometry \n| Frame size number | 49 cm | 52 cm | 54 cm | 56 cm | 58 cm | 61 cm |\n|---------------------------------------|-------|-------|-------|-------|-------|-------|\n| Wheel size | 700c | 700c | 700c | 700c | 700c | 700c |\n| A — Seat tube | 47.5 | 50.0 | 52.0 | 54.0 | 56.0 | 58.5 |\n| B — Seat tube angle | 75.0° | 74.5° | 74.0° | 73.5° | 73.0° | 72.5° |\n| C — Head tube length | 12.0 | 14.5 | 16.5 | 18.5 | 20.5 | 23.5 |\n| D — Head angle | 70.0° | 71.0° | 71.5° | 72.0° | 72.5° | 72.5° |\n| E — Effective top tube | 52.5 | 53.5 | 54.5 | 56.0 | 57.5 | 59.5 |\n| G — Bottom bracket drop | 7.0 | 7.0 | 7.0 | 7.0 | 7.0 | 7.0 |\n| H — Chainstay length | 41.5 | 41.5 | 41.5 | 41.5 | 41.5 | 41.5 |\n| K — Wheelbase | 98.4 | 98.9 | 99.8 | 100.8 | 101.7 | 103.6 |\n| L — Standover | 72.0 | 74.0 | 76.0 | 78.0 | 80.0 | 82.0 |\n| M — Frame reach | 36.2 | 36.8 | 37.3 | 38.1 | 38.6 | 39.4 |\n| N — Frame stack | 52.0 | 54.3 | 56.2 | 58.1 | 59.8 | 62.4 |\n| Saddle rail height min | 67.0 | 69.5 | 71.5 | 74.0 | 76.0 | 78.0 |\n| Saddle rail height max | 75.0 | 77.5 | 79.5 | 82.0 | 84.0 | 86.0 |", + "price": 399.99, + "tags": [ + "bicycle", + "city bike" + ] + }, + { + "name": "Velocity V8", + "shortDescription": "Velocity V8 is a high-performance road bike that is designed to deliver speed, agility, and control on the road. With its lightweight aluminum frame, carbon fiber fork, Shimano Tiagra groupset, and hydraulic disc brakes, this bike is perfect for experienced riders who are looking for a fast and responsive bike that can handle various road conditions.", + "description": "## Overview\n\nIt's right for you if... \nYou are an experienced rider who is looking for a high-performance road bike that is lightweight, agile, and responsive. You want a bike that can handle long-distance rides, steep climbs, and fast descents with ease. You also appreciate the latest technology and reliable components that make your riding experience more enjoyable.\n\nThe tech you get \nVelocity V8 features a lightweight aluminum frame with a carbon fiber fork that ensures a comfortable ride without sacrificing stiffness and power transfer. It comes with a Shimano Tiagra groupset with 10-speed gearing for precise and efficient shifting. Hydraulic disc brakes offer superior stopping power in all weather conditions, while 28mm wide tires provide comfort and stability on various road surfaces. Internal cable routing enhances the bike's sleek appearance.\n\nThe final word \nIf you are looking for a high-performance road bike that is lightweight, fast, and responsive, Velocity V8 is the perfect choice. With its lightweight aluminum frame, reliable components, and advanced technology, this bike is designed to help you enjoy fast and comfortable rides on the road.\n\n## Features \n\nLightweight and responsive \nVelocity V8 comes with a lightweight aluminum frame that is not only lightweight but also responsive, providing excellent handling and control.\n\nHydraulic disc brakes \nThis bike is equipped with hydraulic disc brakes that provide superior stopping power in all weather conditions, ensuring your safety and confidence on the road.\n\nComfortable rides \nThe 28mm wide tires and carbon fork provide ample cushioning, ensuring a smooth and comfortable ride over long distances.\n\nSleek appearance \nThe bike's internal cable routing enhances its sleek appearance while also protecting the cables from the elements, ensuring smooth shifting for longer periods.\n\n## Specifications \n\nFrameset \nFrame\tVelocity V8 Aluminum Frame, Internal Cable Routing, Tapered Headtube, Press Fit Bottom Bracket, 12x142mm Thru-Axle \nFork\tVelocity V8 Carbon Fiber Fork, Internal Brake Routing, 12x100mm Thru-Axle \n\nWheels \nWheelset\tAlexRims CXD7 Wheelset \nTire\tSchwalbe Durano Plus 700x28mm \nInner Tubes\tSchwalbe SV15 700x18-28mm \nSkewers\tVelocity V8 Thru-Axle Skewers \n\nDrivetrain \nShifter\tShimano Tiagra Hydraulic Disc Shifters \nFront Derailleur\tShimano Tiagra \nRear Derailleur\tShimano Tiagra \nCrankset\tShimano Tiagra 50-34T \nBottom Bracket\tShimano BB-RS500-PB \nCassette\tShimano Tiagra 11-32T \nChain\tShimano HG54 10-Speed Chain \n\nComponents \nSaddle\tVelocity V8 Saddle \nSeatpost\tVelocity V8 Aluminum Seatpost \nHandlebar\tVelocity V8 Compact Handlebar \nStem\tVelocity V8 Aluminum Stem \nHeadset\tFSA Orbit IS-2 \n\nBrakes \nBrakes\tShimano Tiagra Hydraulic Disc Brakes \nRotors\tShimano SM-RT64 160mm Rotors \n\nAccessories \nPedals\tVelocity V8 Road Pedals \n\nWeight \nWeight\t9.4 kg / 20.7 lbs \nWeight Limit\tThis bike has a maximum total weight limit (combined weight of bicycle, rider, and cargo) of 120 kg (265 lbs).\n\n## Sizing \n| Size | Rider Height | Inseam |\n|:----:|:-------------------------:|:--------------------:|\n| 49 | 155 - 162 cm 5'1\" - 5'4\" | 71 - 76 cm 28\" - 30\" |\n| 52 | 162 - 170 cm 5'4\" - 5'7\" | 74 - 79 cm 29\" - 31\" |\n| 54 | 170 - 178 cm 5'7\" - 5'10\" | 77 - 83 cm 30\" - 32\" |\n| 56 | 178 - 185 cm 5'10\" - 6'1\" | 82 - 88 cm 32\" - 34\" |\n| 58 | 185 - 193 cm 6'1\" - 6'4\" | 86 - 92 cm 34\" - 36\" |\n| 61 | 193 - 200 cm 6'4\" - 6'7\" | 90 - 95 cm 35\" - 37\" |\n\n## Geometry \n| Frame size number | 49 cm | 52 cm | 54 cm | 56 cm | 58 cm | 61 cm |\n|---------------------------------------|-------|-------|-------|-------|-------|-------|\n| Wheel size | 700c | 700c | 700c | 700c | 700c | 700c |\n| A — Seat tube | 47.5 | 50.0 | 52.0 | 54.0 | 56.0 | 58.5 |\n| B — Seat tube angle | 75.0° | 74.5° | 74.0° | 73.5° | 73.0° | 72.5° |\n| C — Head tube length | 12.0 | 14.5 | 16.5 | 18.5 | 20.5 | 23.5 |\n| D — Head angle | 70.0° | 71.0° | 71.5° | 72.0° | 72.5° | 72.5° |\n| E — Effective top tube | 52.5 | 53.5 | 54.5 | 56.0 | 57.5 | 59.5 |\n| G — Bottom bracket drop | 7.0 | 7.0 | 7.0 | 7.0 | 7.0 | 7.0 |\n| H — Chainstay length | 41.5 | 41.5 | 41.5 | 41.5 | 41.5 | 41.5 |\n| K — Wheelbase | 98.4 | 98.9 | 99.8 | 100.8 | 101.7 | 103.6 |\n| L — Standover | 72.0 | 74.0 | 76.0 | 78.0 | 80.0 | 82.0 |\n| M — Frame reach | 36.2 | 36.8 | 37.3 | 38.1 | 38.6 | 39.4 |\n| N — Frame stack | 52.0 | 54.3 | 56.2 | 58.1 | 59.8 | 62.4 |\n| Saddle rail height min | 67.0 | 69.5 | 71.5 | 74.0 | 76.0 | 78.0 |\n| Saddle rail height max | 75.0 | 77.5 | 79.5 | 82.0 | 84.0 | 86.0 |", + "price": 1899.99, + "tags": [ + "bicycle", + "electric bike" + ] + }, + { + "name": "VeloCore X9 eMTB", + "shortDescription": "The VeloCore X9 eMTB is a light, agile and versatile electric mountain bike designed for adventure and performance. Its purpose-built frame and premium components offer an exhilarating ride experience on both technical terrain and smooth singletrack.", + "description": "## Overview\nIt's right for you if...\nYou love exploring new trails and testing your limits on challenging terrain. You want an electric mountain bike that offers power when you need it, without sacrificing performance or agility. You're looking for a high-quality bike with top-notch components and a sleek design.\n\nThe tech you get\nA lightweight, full carbon frame with custom geometry, a 140mm RockShox Pike Ultimate fork with Charger 2.1 damper, and a Fox Float DPS Performance shock. A Shimano STEPS E8000 motor and 504Wh battery that provide up to 62 miles of range and 20 mph assistance. A Shimano XT 12-speed drivetrain, Shimano SLX brakes, and DT Swiss wheels.\n\nThe final word\nThe VeloCore X9 eMTB delivers power and agility in equal measure. It's a versatile and capable electric mountain bike that can handle any trail with ease. With premium components, a custom carbon frame, and a sleek design, this bike is built for adventure.\n\n## Features\nAgile and responsive\n\nThe VeloCore X9 eMTB is designed to be nimble and responsive on the trail. Its custom carbon frame offers a perfect balance of stiffness and compliance, while the suspension system provides smooth and stable performance on technical terrain.\n\nPowerful and efficient\n\nThe Shimano STEPS E8000 motor and 504Wh battery provide up to 62 miles of range and 20 mph assistance. The motor delivers smooth and powerful performance, while the battery offers reliable and consistent power for long rides.\n\nCustomizable ride experience\n\nThe VeloCore X9 eMTB comes with an intuitive and customizable Shimano STEPS display that allows you to adjust the level of assistance, monitor your speed and battery life, and customize your ride experience to suit your needs.\n\nPremium components\n\nThe VeloCore X9 eMTB is equipped with high-end components, including a Shimano XT 12-speed drivetrain, Shimano SLX brakes, and DT Swiss wheels. These components offer reliable and precise performance, allowing you to push your limits with confidence.\n\n## Specs\nFrameset\nFrame\tVeloCore carbon fiber frame, Boost, tapered head tube, internal cable routing, 140mm travel\nFork\tRockShox Pike Ultimate, Charger 2.1 damper, DebonAir spring, 15x110mm Boost Maxle Ultimate, 46mm offset, 140mm travel\nShock\tFox Float DPS Performance, EVOL, 3-position adjust, Kashima Coat, 210x50mm\n\nWheels\nWheel front\tDT Swiss XM1700 Spline, 30mm internal width, 15x110mm Boost axle\nWheel rear\tDT Swiss XM1700 Spline, 30mm internal width, Shimano Microspline driver, 12x148mm Boost axle\nTire front\tMaxxis Minion DHF, 29x2.5\", EXO+ casing, tubeless ready\nTire rear\tMaxxis Minion DHR II, 29x2.4\", EXO+ casing, tubeless ready\n\nDrivetrain\nShifter\tShimano XT M8100, 12-speed\nRear derailleur\tShimano XT M8100, Shadow Plus, long cage, 51T max cog\nCrankset\tShimano STEPS E8000, 165mm length, 34T chainring\nCassette\tShimano XT M8100, 10-51T, 12-speed\nChain\tShimano CN-M8100, 12-speed\nPedals\tNot included\n\nComponents\nSaddle\tBontrager Arvada, hollow chromoly rails\nSeatpost\tDrop Line, internal routing, 31.6mm (15.5: 100mm, 17.5 & 18.5: 125mm, 19.5 & 21.5: 150mm)\nHandlebar\tBontrager Line Pro, ADV Carbon, 35mm, 27.5mm rise, 780mm width\nStem\tBontrager Line Pro, 35mm, Knock Block, 0 degree, 50mm length\nGrips\tBontrager XR Trail Elite, alloy lock-on\nHeadset\tIntegrated, sealed cartridge bearing, 1-1/8\" top, 1.5\" bottom\nBrakeset\tShimano SLX M7120, 4-piston hydraulic disc\n\nAccessories\nBattery\tShimano STEPS BT-E8010, 504Wh\nCharger\tShimano STEPS EC-E8004, 4A\nController\tShimano STEPS E8000 display\nBike weight\tM - 22.5 kg / 49.6 lbs (with tubes)\n\n## Sizing & fit\n\n| Size | Rider Height |\n|:----:|:------------------------:|\n| S | 162 - 170 cm 5'4\" - 5'7\" |\n| M | 170 - 178 cm 5'7\" - 5'10\"|\n| L | 178 - 186 cm 5'10\" - 6'1\"|\n| XL | 186 - 196 cm 6'1\" - 6'5\" |\n\n## Geometry\n\nAll measurements provided in cm unless otherwise noted.\n\n| Frame size | S | M | L | XL |\n|---------------------------|-------|-------|-------|-------|\n| A — Seat tube | 40.6 | 43.2 | 47.0 | 51.0 |\n| B — Seat tube angle | 75.0° | 75.0° | 75.0° | 75.0° |\n| C — Head tube length | 9.6 | 10.6 | 11.6 | 12.6 |\n| D — Head angle | 66.5° | 66.5° | 66.5° | 66.5° |\n| E — Effective top tube | 60.4 | 62.6 | 64.8 | 66.9 |\n| F — Bottom bracket height | 33.2 | 33.2 | 33.2 | 33.2 |\n| G — Bottom bracket drop | 3.0 | 3.0 | 3.0 | 3.0 |\n| H — Chainstay length | 45.5 | 45.5 | 45.5 | 45.5 |\n| I — Offset | 4.6 | 4.6 | 4.6 | 4.6 |\n| J — Trail | 11.9 | 11.9 | 11.9 | 11.9 |\n| K — Wheelbase | 117.0 | 119.3 | 121.6 | 123.9 |\n| L — Standover | 75.9 | 75.9 | 78.6 | 78.6 |\n| M — Frame reach | 43.6 | 45.6 | 47.6 | 49.6 |\n| N — Frame stack | 60.5 | 61.5 | 62.4 | 63.4 |", + "price": 1299.99, + "tags": [ + "bicycle", + "touring bike" + ] + }, + { + "name": "Zephyr 8.8 GX Eagle AXS Gen 3", + "shortDescription": "Zephyr 8.8 GX Eagle AXS is a light and nimble full-suspension mountain bike. It's designed to handle technical terrain with ease and has a smooth and efficient ride feel. The sleek and powerful Bosch Performance Line CX motor and removable Powertube battery provide a boost to your pedaling and give you long-lasting riding time. The bike also features high-end components and advanced technology for an ultimate mountain biking experience.", + "description": "## Overview\nIt's right for you if...\nYou're an avid mountain biker looking for a high-performance e-MTB that can tackle challenging trails. You want a bike with a powerful motor, efficient suspension, and advanced technology to enhance your riding experience. You also need a bike that's reliable and durable for long-lasting use.\n\nThe tech you get\nA lightweight, full carbon frame with 150mm of rear travel and a 160mm RockShox Pike Ultimate fork with Charger 2.1 RCT3 damper, remote lockout, and DebonAir spring. A Bosch Performance Line CX motor and removable Powertube 625Wh battery that can assist up to 20mph when it's on and gives zero drag when it's off, plus an easy-to-use handlebar-mounted Bosch Purion controller. A SRAM GX Eagle AXS wireless electronic drivetrain, a RockShox Reverb Stealth dropper, and DT Swiss HX1501 Spline One wheels.\n\nThe final word\nZephyr 8.8 GX Eagle AXS is a high-performance e-MTB that's designed to handle technical terrain with ease. With a powerful Bosch motor and long-lasting battery, you can conquer challenging climbs and enjoy long rides. The bike also features high-end components and advanced technology for an ultimate mountain biking experience.\n\n## Features\nPowerful motor\n\nThe Bosch Performance Line CX motor provides a boost to your pedaling and can assist up to 20mph. It has four power modes and a walk-assist function for easy navigation on steep climbs. The motor is also reliable and durable for long-lasting use.\n\nEfficient suspension\n\nZephyr 8.8 has a 150mm of rear travel and a 160mm RockShox Pike Ultimate fork with Charger 2.1 RCT3 damper, remote lockout, and DebonAir spring. The suspension is efficient and responsive, allowing you to handle technical terrain with ease.\n\nRemovable battery\n\nThe Powertube 625Wh battery is removable for easy charging and storage. It provides long-lasting riding time and can be replaced with a spare battery for even longer rides. The battery is also durable and weather-resistant for all-season riding.\n\nAdvanced technology\n\nZephyr 8.8 is equipped with advanced technology, including a Bosch Purion controller for easy motor control, a SRAM GX Eagle AXS wireless electronic drivetrain for precise shifting, and a RockShox Reverb Stealth dropper for adjustable saddle height. The bike also has DT Swiss HX1501 Spline One wheels for reliable performance on any terrain.\n\nCarbon frame\n\nThe full carbon frame is lightweight and durable, providing a smooth and efficient ride. It's also designed with a tapered head tube, internal cable routing, and Boost148 spacing for enhanced stiffness and responsiveness.\n\n## Specs\nFrameset\nFrame\tCarbon main frame & stays, tapered head tube, internal routing, Boost148, 150mm travel\nFork\tRockShox Pike Ultimate, Charger 2.1 RCT3 damper, DebonAir spring, remote lockout, tapered steerer, Boost110, 15mm Maxle Stealth, 160mm travel\nShock\tRockShox Deluxe RT3, DebonAir spring, 205mm x 57.5mm\nMax compatible fork travel\t170mm\n\nWheels\nWheel front\tDT Swiss HX1501 Spline One, Centerlock, 30mm inner width, 110x15mm Boost\nWheel rear\tDT Swiss HX1501 Spline One, Centerlock, 30mm inner width, SRAM XD driver, 148x12mm Boost\nTire\tBontrager XR4 Team Issue, Tubeless Ready, Inner Strength sidewall, aramid bead, 120tpi, 29x2.40''\nMax tire size\t29x2.60\"\n\nDrivetrain\nShifter\tSRAM GX Eagle AXS, wireless, 12 speed\nRear derailleur\tSRAM GX Eagle AXS\nCrank\tBosch Gen 4, 32T\nChainring\tSRAM X-Sync 2, 32T, direct-mount\nCassette\tSRAM PG-1275 Eagle, 10-52, 12 speed\nChain\tSRAM GX Eagle, 12 speed\n\nComponents\nSaddle\tBontrager Arvada, hollow titanium rails, 138mm width\nSeatpost\tRockShox Reverb Stealth, 31.6mm, internal routing, 150mm (S), 170mm (M/L), 200mm (XL)\nHandlebar\tBontrager Line Pro, ADV Carbon, 35mm, 27.5mm rise, 780mm width\nGrips\tBontrager XR Trail Elite, alloy lock-on\nStem\tBontrager Line Pro, Knock Block, 35mm, 0 degree, 50mm length\nHeadset\tIntegrated, sealed cartridge bearing, 1-1/8'' top, 1.5'' bottom\nBrake\tSRAM Code RSC hydraulic disc, 200mm (front), 180mm (rear)\nBrake rotor\tSRAM CenterLine, centerlock, round edge, 200mm (front), 180mm (rear)\n\nAccessories\nE-bike system\tBosch Performance Line CX\nBattery\tBosch Powertube 625Wh\nCharger\tBosch 4A compact charger\nController\tBosch Purion\nTool\tBontrager multi-tool, integrated storage bag\n\nWeight\nWeight\tM - 24.08 kg / 53.07 lbs (with TLR sealant, no tubes)\nWeight limit\tThis bike has a maximum total weight limit (combined weight of bicycle, rider, and cargo) of 300 pounds (136 kg).\n\n\n## Sizing & fit\n\n| Size | Rider Height | Inseam |\n|:----:|:------------------------:|:--------------------:|\n| S | 153 - 162 cm 5'0\" - 5'4\" | 67 - 74 cm 26\" - 29\" |\n| M | 161 - 172 cm 5'3\" - 5'8\" | 74 - 79 cm 29\" - 31\" |\n| L | 171 - 180 cm 5'7\" - 5'11\" | 79 - 84 cm 31\" - 33\" |\n| XL | 179 - 188 cm 5'10\" - 6'2\" | 84 - 89 cm 33\" - 35\" |\n\n\n## Geometry\n\nAll measurements provided in cm unless otherwise noted.\nSizing table\n| Frame size letter | S | M | L | XL |\n|---------------------------|-------|-------|-------|-------|\n| Actual frame size | 15.5 | 17.5 | 19.5 | 21.5 |\n| Wheel size | 29\" | 29\" | 29\" | 29\" |\n| A — Seat tube | 39.4 | 41.9 | 44.5 | 47.6 |\n| B — Seat tube angle | 76.1° | 76.1° | 76.1° | 76.1° |\n| C — Head tube length | 9.6 | 10.5 | 11.5 | 12.5 |\n| D — Head angle | 65.5° | 65.5° | 65.5° | 65.5° |\n| E — Effective top tube | 58.6 | 61.3 | 64.0 | 66.7 |\n| F — Bottom bracket height | 34.0 | 34.0 | 34.0 | 34.0 |\n| G — Bottom bracket drop | 1.0 | 1.0 | 1.0 | 1.0 |\n| H — Chainstay length | 45.0 | 45.0 | 45.0 | 45.0 |\n| I — Offset | 4.6 | 4.6 | 4.6 | 4.6 |\n| J — Trail | 10.5 | 10.5 | 10.5 | 10.5 |\n| K — Wheelbase | 119.5 | 122.3 | 125.0 | 127.8 |\n| L — Standover | 72.7 | 74.7 | 77.6 | 81.0 |\n|", + "price": 1499.99, + "tags": [ + "bicycle", + "electric bike", + "city bike" + ] + }, + { + "name": "Velo 99 XR1 AXS", + "shortDescription": "Velo 99 XR1 AXS is a next-generation bike designed for fast-paced adventure seekers and speed enthusiasts. Built for high-performance racing, the bike boasts state-of-the-art technology and premium components. It is the ultimate bike for riders who want to push their limits and get their adrenaline pumping.", + "description": "## Overview\nIt's right for you if...\nYou are a passionate cyclist looking for a bike that can keep up with your speed, agility, and endurance. You are an adventurer who loves to explore new terrains and challenge yourself on the toughest courses. You want a bike that is lightweight, durable, and packed with the latest technology.\n\nThe tech you get\nA lightweight, full carbon frame with advanced aerodynamics and integrated cable routing for a clean look. A high-performance SRAM XX1 Eagle AXS wireless electronic drivetrain, featuring a 12-speed cassette and a 32T chainring. A RockShox SID Ultimate fork with a remote lockout, 120mm travel, and Charger Race Day damper. A high-end SRAM G2 Ultimate hydraulic disc brake with carbon levers. A FOX Transfer SL dropper post for quick and easy height adjustments. DT Swiss XRC 1501 carbon wheels for superior speed and handling.\n\nThe final word\nVelo 99 XR1 AXS is a premium racing bike that can help you achieve your goals and reach new heights. It is designed for speed, agility, and performance, and it is packed with the latest technology and premium components. If you are a serious cyclist who wants the best, this is the bike for you.\n\n## Features\nAerodynamic design\n\nThe Velo 99 XR1 AXS features a state-of-the-art frame design that reduces drag and improves speed. It has an aerodynamic seatpost, integrated cable routing, and a sleek, streamlined look that sets it apart from other bikes.\n\nWireless electronic drivetrain\n\nThe SRAM XX1 Eagle AXS drivetrain features a wireless electronic system that provides precise, instant shifting and unmatched efficiency. It eliminates the need for cables and makes the bike lighter and faster.\n\nHigh-performance suspension\n\nThe RockShox SID Ultimate fork and Charger Race Day damper provide 120mm of smooth, responsive suspension that can handle any terrain. The fork also has a remote lockout for quick adjustments on the fly.\n\nSuperior braking power\n\nThe SRAM G2 Ultimate hydraulic disc brake system delivers unmatched stopping power and control. It has carbon levers for a lightweight, ergonomic design and precision control.\n\nCarbon wheels\n\nThe DT Swiss XRC 1501 carbon wheels are ultra-lightweight, yet incredibly strong and durable. They provide superior speed and handling, making the bike more agile and responsive.\n\n## Specs\nFrameset\nFrame\tFull carbon frame, integrated cable routing, aerodynamic design, Boost148\nFork\tRockShox SID Ultimate, Charger Race Day damper, remote lockout, tapered steerer, Boost110, 15mm Maxle Stealth, 120mm travel\n\nWheels\nWheel front\tDT Swiss XRC 1501 carbon wheel, Boost110, 15mm thru axle\nWheel rear\tDT Swiss XRC 1501 carbon wheel, SRAM XD driver, Boost148, 12mm thru axle\nTire\tSchwalbe Racing Ray, Performance Line, Addix, 29x2.25\"\nTire part\tSchwalbe Doc Blue Professional, 500ml\nMax tire size\t29x2.3\"\n\nDrivetrain\nShifter\tSRAM Eagle AXS, wireless, 12-speed\nRear derailleur\tSRAM XX1 Eagle AXS\nCrank\tSRAM XX1 Eagle, 32T, carbon\nChainring\tSRAM X-SYNC, 32T, alloy\nCassette\tSRAM Eagle XG-1299, 10-52, 12-speed\nChain\tSRAM XX1 Eagle, 12-speed\nMax chainring size\t1x: 32T\n\nComponents\nSaddle\tBontrager Montrose Elite, carbon rails, 138mm width\nSeatpost\tFOX Transfer SL, 125mm travel, internal routing, 31.6mm\nHandlebar\tBontrager Kovee Pro, ADV Carbon, 35mm, 5mm rise, 720mm width\nGrips\tBontrager XR Endurance Elite\nStem\tBontrager Kovee Pro, 35mm, Blendr compatible, 7 degree, 60mm length\nHeadset\tIntegrated, cartridge bearing, 1-1/8\" top, 1.5\" bottom\nBrake\tSRAM G2 Ultimate hydraulic disc, carbon levers, 180mm rotors\n\nAccessories\nBike computer\tBontrager Trip 300\nTool\tBontrager Flatline Pro pedal wrench, T25 Torx\n\n\n## Sizing & fit\n\n| Size | Rider Height | Inseam |\n|:----:|:------------------------:|:--------------------:|\n| S | 158 - 168 cm 5'2\" - 5'6\" | 74 - 78 cm 29\" - 31\" |\n| M | 165 - 175 cm 5'5\" - 5'9\" | 78 - 82 cm 31\" - 32\" |\n| L | 173 - 183 cm 5'8\" - 6'0\" | 82 - 86 cm 32\" - 34\" |\n| XL | 180 - 193 cm 5'11\" - 6'4\" | 86 - 90 cm 34\" - 35\" |\n\n\n## Geometry\n\nAll measurements provided in cm unless otherwise noted.\nSizing table\n| Frame size letter | S | M | L | XL |\n|---------------------------|-------|-------|-------|-------|\n| Actual frame size | 15.5 | 17.5 | 19.5 | 21.5 |\n| Wheel size | 29\" | 29\" | 29\" | 29\" |\n| A — Seat tube | 39.9 | 43.0 | 47.0 | 51.0 |\n| B — Seat tube angle | 74.5° | 74.5° | 74.5° | 74.5° |\n| C — Head tube length | 9.0 | 10.0 | 11.0 | 12.0 |\n| D — Head angle | 68.0° | 68.0° | 68.0° | 68.0° |\n| E — Effective top tube | 57.8 | 59.7 | 61.6 | 63.6 |\n| F — Bottom bracket height | 33.0 | 33.0 | 33.0 | 33.0 |\n| G — Bottom bracket drop | 5.0 | 5.0 | 5.0 | 5.0 |\n| H — Chainstay length | 43.0 | 43.0 | 43.0 | 43.0 |\n| I — Offset | 4.2 | 4.2 | 4.2 | 4.2 |\n| J — Trail | 9.7 | 9.7 | 9.7 | 9.7 |\n| K — Wheelbase | 112.5 | 114.5 | 116.5 | 118.6 |\n| L — Standover | 75.9 | 77.8 | 81.5 | 84.2 |\n| M — Frame reach | 41.6 | 43.4 | 45.2 | 47.1 |\n| N — Frame stack | 58.2 | 58.9 | 59.3 | 59.9 |", + "price": 1099.99, + "tags": [ + "bicycle", + "mountain bike" + ] + } +] \ No newline at end of file