diff --git a/docs.json b/docs.json index d7631a40..854eeb3c 100644 --- a/docs.json +++ b/docs.json @@ -27,6 +27,10 @@ "observe-functions" ] }, + { + "group": "Tutorials", + "pages": ["semantic-search"] + }, { "group": "Manage", "pages": [ diff --git a/images/tutorials/semantic-search/upsert.png b/images/tutorials/semantic-search/upsert.png new file mode 100644 index 00000000..fdf01cd9 Binary files /dev/null and b/images/tutorials/semantic-search/upsert.png differ diff --git a/semantic-search.mdx b/semantic-search.mdx new file mode 100644 index 00000000..eae1ae2a --- /dev/null +++ b/semantic-search.mdx @@ -0,0 +1,396 @@ +--- +title: Semantic Search With Dgraph and Modus +description: + "Add natural language search to your app with Dgraph, Modus, and AI embeddings" +--- + +By leveraging embeddings and similarity search backed by a scalable vector index +developers can enable semantic and similarity-based searches, improving the +relevance of search results within their applications. This tutorial covers +implementing semantic search using Modus, Dgraph, and Hypermode hosted AI models +to add natural language or semantic search to your app using an example of +ecommerce product data. + + + +## Semantic search overview + +Semantic search focuses on understanding the meaning and context behind queries +rather than just matching keywords, using embeddings to capture semantic +relationships between concepts. Vector search serves as the technical +implementation method, converting text into numerical vector embeddings and +finding similar content through mathematical distance calculations in +multidimensional space. + +Vector search is a powerful technique that transforms data (like text, images, +or audio) into numerical representations called embeddings. These embeddings +capture the semantic meaning of the content in a multi-dimensional space and +position similar items closer together. When performing a search, the query is +also converted into an embedding, and the system finds items whose embeddings +are closest to the query embedding. + +This approach offers significant benefits over traditional keyword-based search, +including improved relevance by capturing context and semantics, enhanced +precision by understanding user intent, and the ability to handle complex +queries with higher accuracy. Vector search is particularly effective for +applications like semantic search, recommendation systems, and retrieval +augmented generation (RAG), optimizing both efficiency and accuracy in finding +and retrieving data based on meaningful similarity rather than exact matches. +When combined with graph traversals, vector search can enable complex GraphRAG +retrieval patterns. + +## The components + +- **Dgraph** is a scalable graph database capable of near real-time graph + traversals and vector search. +- **Modus** is the serverless API framework for building AI applications. +- **Hypermode** is the platform for deploying AI applications, including model + hosting. + +## Prerequisites + +This tutorial assumes you have: + +1. Created a Dgraph instance, either hosted in the cloud or locally via Docker + or installed via the Dgraph binary +2. Installed the Modus CLI and created a Modus app. See the + [Modus Quickstart](modus/quickstart) to get started with Modus. + +## Natural language search with Dgraph and Modus + +The steps to implement natural language or semantic search with Dgraph include +defining the Dgraph connection in your Modus app manifest, selecting and +configuring an embedding model, declaring a vector index in the Dgraph DQL +schema, and using the `similar_to` DQL function to search for similar text in +vector space. + +Our example app uses ecommerce product data consisting of product details to +enable semantic product search based on natural language terms. + +## Declare Dgraph connection and Hypermode embedding model + +First, update the Modus app manifest file `modus.json` to define the connection +to your Dgraph instance and the embedding model used to generate embeddings. +Here we're using the MiniLM model hosted by Hypermode and connecting to a +locally running Dgraph instance. + +```json +{ + "$schema": "https://schema.hypermode.com/modus.json", + "endpoints": { + "default": { + "type": "graphql", + "path": "/graphql", + "auth": "bearer-token" + } + }, + "models": { + "minilm": { + "sourceModel": "sentence-transformers/all-MiniLM-L6-v2", + "connection": "hypermode", + "provider": "hugging-face" + } + }, + "connections": { + "dgraph-grpc": { + "type": "dgraph", + "grpcTarget": "localhost:9080" + } + } +} +``` + + + In order to use Hypermode hosted models in the local Modus development + environment you'll need to use the `hyp` CLI to connect your local environment + with your Hypermode account. See the [Using Hypermode-hosted + models](work-locally#using-hypermode-hosted-models) docs page for more + information. + + +## Type definitions + +Next, in our Modus app we define our data model using classes with decorators +for automatic serialization/deserialization. The `@json` decorator enables JSON +serialization, while `@alias` maps property names to Dgraph convention friendly +formats. + +We'll be using ecommerce data so we'll create simple types defining products and +their categories. + +```ts +@json +export class Product { + @alias("Product.id") + id!: string + + @alias("Product.title") + title: string = "" + + @alias("Product.description") + description: string = "" + + @alias("Product.category") + @omitnull() + category: Category | null = null +} + +@json +export class Category { + @alias("Category.name") + name: string = "" +} +``` + +## Embedding model integration + +Next, we create an embedding function that uses a transformer model (MiniLM in +this case) to convert product descriptions and search queries into vectors: + +```ts +import { models } from "@hypermode/modus-sdk-as" +import { EmbeddingsModel } from "@hypermode/modus-sdk-as/models/experimental/embeddings" + +const EMBEDDING_MODEL = "minilm" + +export function embedText(content: string[]): f32[][] { + const model = models.getModel(EMBEDDING_MODEL) + const input = model.createInput(content) + const output = model.invoke(input) + return output.predictions +} +``` + +## Define Dgraph schema + +We declare a schema to use Dgraph's vector search capability and create an index +on the `Product.embedding` property, even though Dgraph can function without a +schema. + +To define our Dgraph schema with vector indexing support we add the +`@index(hnsw)` directive to the property storing the embedding value, in this +case `Product.embedding`. We also define the other property types and node +labels. + +```rdf +: string @index(hash) . +: uid @reverse . +: string . +: string @index(hash) . +: float32vector @index(hnsw) . +``` + +To apply this schema to our Dgraph instance we can make a POST request to the +`/alter` endpoint of our Dgraph instance: + +```bash +curl -X POST localhost:8080/alter --silent --data-binary '@dqlschema.txt' +``` + +or use the schema tab of the Ratel interface to apply the schema. + +## Define Modus mutation function + +Now we're ready to create a Modus function to create data in Dgraph. Here we +create an upsert mutation that creates a product and related category in Dgraph, +without creating duplicate nodes. + +This function uses the embedding model we configured in previous steps to create +an embedding of the product description and save to the `Product.embedding` +property. + +```ts +const DGRAPH_CONNECTION = "dgraph-grpc" + +/** + * Add or update a new product to the database + */ +export function upsertProduct(product: Product): Map | null { + let payload = buildProductMutationJson(DGRAPH_CONNECTION, product) + + const embedding = embedText([product.description])[0] + payload = addEmbeddingToJson(payload, "Product.embedding", embedding) + + const mutation = new dgraph.Mutation(payload) + const response = dgraph.executeMutations(DGRAPH_CONNECTION, mutation) + + return response.Uids +} +``` + + + Refer to the full code + [here](https://github.com/hypermodeinc/modus-recipes/tree/main/dgraph-101) for + how to implement other Dgraph mutation and query functions and associated + Dgraph helpers. + + +## Dgraph `similar_to` query function + +Next, we create a Modus function that uses Dgraph's `similar_to` query function +that leverages the vector index to find semantically similar products by +computing an embedding of the search term and searching for nearby product +descriptions in vector space. + +```ts +/** + * Search products by similarity to a given text + */ +export function searchProducts(search: string): Product[] { + const embedding = embedText([search])[0] + const topK = 3 + const body = ` + Product.id + Product.description + Product.title + Product.category { + Category.name + } + ` + return searchBySimilarity( + DGRAPH_CONNECTION, + embedding, + "Product.embedding", + body, + topK, + ) +} + +export function searchBySimilarity( + connection: string, + embedding: f32[], + predicate: string, + body: string, + topK: i32, +): T[] { + const query = new dgraph.Query(` + query search($vector: float32vector) { + var(func: similar_to(${predicate},${topK},$vector)) { + vemb as Product.embedding + dist as math((vemb - $vector) dot (vemb - $vector)) + score as math(1 - (dist / 2.0)) + } + + list(func:uid(score),orderdesc:val(score)) @filter(gt(val(score),0.25)){ + ${body} + } + }`).withVariable("$vector", embedding) + + const response = dgraph.executeQuery(connection, query) + console.log(response.Json) + return JSON.parse>(response.Json).list +} +``` + +## Query Modus endpoint + +We can run our Modus app using the `modus dev` command which generates a GraphQL +schema from the functions we've defined and start a local GraphQL endpoint for +testing and development. + +Navigate to `http://localhost:8686/explorer` in your browser and use the Modus +API Explorer to first insert sample data into Dgraph using the upsert mutation +function we defined previously and then search for similar products using vector +search. + +First, to create product and category nodes: + +```graphql +mutation ($product: ProductInput!) { + upsertProduct(product: $product) { + key + value + } +} +``` + +We'll use the following values for the `product` variable creating three product +nodes ad their associated category nodes in Dgraph: + +| Product ID | Title | Description | Category | +| ---------- | ----------------------- | -------------------------------------------------------------------------------------------------- | ------------------ | +| P001 | Solar-Powered Umbrella | A stylish umbrella with solar panels that charge your devices while you walk. | Outdoor Gear | +| P002 | Self-Warming Coffee Mug | A mug that keeps your coffee at the perfect temperature using smart heating technology. | Kitchen Appliances | +| P003 | Smart Pillow 2.0 | A pillow that tracks your sleep patterns and plays soothing sounds to help you fall asleep faster. | Smart Home | + +![Creating products in Dgraph](images/tutorials/semantic-search/upsert.png) + +And then to search for semantically similar products based on a search string we +can execute the following query, using the value of our search string for the +`$search` variable. + +```graphql +query ($search: String!) { + searchProducts(search: $search) { + id + title + description + category { + name + } + } +} +``` + +For example, if we search using the search term "rain": + +```graphql +query { + searchProducts(search: "rain") { + id + title + description + category { + name + } + } +} +``` + +the product search results returns our solar powered umbrella. + +```json +{ + "data": { + "searchProducts": [ + { + "id": "P001", + "title": "Solar-Powered Umbrella", + "description": "A stylish umbrella with solar panels that charge your devices while you walk.", + "category": { + "name": "Outdoor Gear" + } + } + ] + } +} +``` + +Even though the description of the solar powered umbrella doesn't include the +word "rain" thanks to the meaning encoded into the embedding our semantic search +process understands the association between rain and umbrella. + +## Next steps + +Now that we've implemented semantic search using Dgraph, Modus, and Hypermode +hosted models using the local development experience we're ready to take the +next step and deploy our project to Hypermode. See the [Deploy Project](deploy) +section for a walk through of this process. + +## Resources + +- You can find the full code for this example in the + [Modus Recipes](https://github.com/hypermodeinc/modus-recipes) GitHub + repository: + [https://github.com/hypermodeinc/modus-recipes/tree/main/dgraph-101](https://github.com/hypermodeinc/modus-recipes/tree/main/dgraph-101) +- Watch a video overview of this tutorial in the + [Hypermode YouTube channel](https://www.youtube.com/@hypermodeinc): + [https://www.youtube.com/watch?v=Z2fB-nBf4Wo](https://www.youtube.com/watch?v=Z2fB-nBf4Wo) diff --git a/styles/Google/Acronyms.yml b/styles/Google/Acronyms.yml index f41af018..ef4c0775 100644 --- a/styles/Google/Acronyms.yml +++ b/styles/Google/Acronyms.yml @@ -41,6 +41,7 @@ exceptions: - PDF - PHP - POST + - RAG - RAM - REPL - RSA