Adding Semantic Search to an Existing DynamoDB Table with Vector Indexes
DEV Community

Adding Semantic Search to an Existing DynamoDB Table with Vector Indexes

Whenever someone asks me to add search to an application, I try to find ways around it. The implementation itself isn't the problem, it's everything that comes with it: extra components to manage, more failure points, and the constant challenge of keeping data in sync. For the past few years, I've worked a lot with DynamoDB and with the introduction of Vector Search I feel a lot more comfortable to add this type of functionality.

I wrote last week about why AWS released another vector store and where DynamoDB Vector Search fits in the landscape. In this post I want to show you how you can take an existing DynamoDB table and add vector search to it.

The Starting Point

The current API uses a serverless setup. It includes SAM for infrastructure, API Gateway in front, Lambda functions behind, and DynamoDB for storage. The API contains plain CRUD operations to manage recipes: create, read, update, delete, and list.

The complexity arises when you want to add filters to query exactly for what you need. In DynamoDB this means you need to add Global Secondary Indexes (GSI) for every permutation... this is really not scalable. So the only option until now was to have a data pipeline to index the data separately and provide search. That is not the case anymore! With vector search in DynamoDB, we can store vector embeddings alongside our data and search them directly. Now, users can search our recipes using natural language queries and find recipes based on the meaning, not just exact keyword matches.

What Is Semantic Search?

Semantic search works by turning text into embeddings, which are lists of numbers that represent the meaning of the text. Two pieces of text that mean similar things end up close together in the vector space, so "spicy chicken stew" lands near "hot and hearty poultry dish" even though they share almost no words. The closeness is what lets you search by intent instead of by keyword.

Adding Vector Embeddings with Amazon Bedrock

To store an embedding we first need to generate one. For that you need an embedding model, which converts your text into a numerical representation. I picked Amazon Bedrock's Titan Text Embeddings V2 model. It produces 1024-dimension vectors, returns them normalized, and pairs naturally with cosine similarity.

The first thing you need to figure out is what text you actually want to embed. Our recipes use structured data, that's why I created a single string that combines key fields: name, description, cuisine, dietary tags, and ingredients. Combining them into a single representation means a search can match on any of those fields at once.

function buildEmbeddingText(recipe: RecipeInput): string {
  const ingredientNames = recipe.ingredients.map((i) => i.name).join(", ");
  const dietaryInfo = recipe.dietary?.length
    ? `Dietary: ${recipe.dietary.join(", ")}.`
    : "";

  return [
    recipe.name,
    recipe.description,
    `Cuisine: ${recipe.cuisine}.`,
    dietaryInfo,
    `Ingredients: ${ingredientNames}.`,
    `Prep time: ${recipe.prepTimeMinutes} minutes. Cook time: ${recipe.cookTimeMinutes} minutes.`,
  ]
    .filter(Boolean)
    .join(" ");
}

async function generateEmbedding(text: string): Promise<number[]> {
  const response = await bedrock.send(
    new InvokeModelCommand({
      modelId: "amazon.titan-embed-text-v2:0",
      contentType: "application/json",
      accept: "application/json",
      body: JSON.stringify({
        inputText: text,
        dimensions: 1024,
        normalize: true,
      }),
    })
  );

  const result = JSON.parse(new TextDecoder().decode(response.body));
  return result.embedding;
}

Because the embedding is generated inline, every item is searchable the moment it's written.

DynamoDB Vector Indexes

The piece that makes this work without a separate service is that DynamoDB now supports vector indexes natively. You store the embedding as an attribute on the item, create a vector index over that attribute, and query it with a dedicated similarity API. It's very similar to how we already create a GSI and call it using the Query command.

Note: Vector index isn't supported by CloudFormation yet, so I couldn't define it in my SAM template. Instead, I added a script that runs after deployment and creates the index using the UpdateTable command if it doesn't already exist.

I created the index with cosine distance, 1024 dimensions to match the Titan output, and an inline filter on cuisine. The inline filter lets you prefilter results, think of it like the partition key in a regular DynamoDB index but without it being required.

await dynamodb.send(
  new UpdateTableCommand({
    TableName: tableName,
    AttributeDefinitions: [
      {
        AttributeName: "cuisine",
        AttributeType: "S",
      },
    ],
    VectorIndexUpdates: [
      {
        Create: {
          IndexName: VECTOR_INDEX_NAME,
          VectorAttribute: {
            AttributeName: "embedding",
          },
          SearchSchema: [
            {
              AttributeName: "cuisine",
              SearchSchemaElement
Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.