Documentation

Semantic Text Search Using LangChain (OpenAI) and Redis

Prasan Kumar
Author
Prasan Kumar, Technical Solutions Developer at Redis
Will Johnston
Author
Will Johnston, Developer Growth Manager at Redis

What you will learn in this tutorial#

Terminology#

Microservices architecture for an e-commerce application#

GITHUB CODE

Below is a command to the clone the source code for the application used in this tutorial

git clone --branch v9.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions

INFO

You don't need to use MongoDB/ Postgresql as your write-optimized database in the demo application; you can use other prisma supported databases as well. This is just an example.

E-commerce application frontend using Next.js and Tailwind#

Database setup#

INFO

Sign up for an OpenAI account to get your API key to be used in the demo (add OPEN_AI_API_KEY variable in .env file). You can also refer to the OpenAI API documentation for more information.

GITHUB CODE

Below is a command to the clone the source code for the application used in this tutorial

git clone --branch v9.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions

Sample data#

database/fashion-dataset/001/products/*.json
const products = [
  {
    productId: '11000',
    price: 3995,
    productDisplayName: 'Puma Men Slick 3HD Yellow Black Watches',
    variantName: 'Slick 3HD Yellow',
    brandName: 'Puma',
    ageGroup: 'Adults-Men',
    gender: 'Men',
    displayCategories: 'Accessories',
    masterCategory_typeName: 'Accessories',
    subCategory_typeName: 'Watches',
    styleImages_default_imageURL:
      'http://host.docker.internal:8080/images/11000.jpg',
    productDescriptors_description_value: 'Stylish and comfortable, ...',
    stockQty: 25,
  },
  //...
];

Seeding product details embeddings#

import { Document } from 'langchain/document';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { RedisVectorStore } from 'langchain/vectorstores/redis';

const convertToVectorDocuments = async (
  _products: Prisma.ProductCreateInput[],
) => {
  const vectorDocs: Document[] = [];

  if (_products?.length > 0) {
    for (let product of _products) {
      let doc = new Document({
        metadata: {
          productId: product.productId,
        },
        pageContent: ` Product details are as follows:
                productId: ${product.productId}.
    
                productDisplayName: ${product.productDisplayName}.
                
                price: ${product.price}.
    
                variantName: ${product.variantName}.
    
                brandName: ${product.brandName}.
    
                ageGroup: ${product.ageGroup}.
    
                gender: ${product.gender}.
    
                productColors: ${product.productColors}
    
                Category:  ${product.displayCategories}, ${product.masterCategory_typeName} - ${product.subCategory_typeName}
    
                productDescription:  ${product.productDescriptors_description_value}`,
      });

      vectorDocs.push(doc);
    }
  }
  return vectorDocs;
};

const seedOpenAIEmbeddings = async (
  vectorDocs: Document[],
  _redisClient: NodeRedisClientType,
  _openAIApiKey: string,
) => {
  if (vectorDocs?.length && _redisClient && _openAIApiKey) {
    console.log('openAIEmbeddings started !');

    const embeddings = new OpenAIEmbeddings({
      openAIApiKey: _openAIApiKey,
    });
    const vectorStore = await RedisVectorStore.fromDocuments(
      vectorDocs,
      embeddings,
      {
        redisClient: _redisClient,
        indexName: 'openAIProductsIdx',
        keyPrefix: 'openAIProducts:',
      },
    );
    console.log('OpenAIEmbeddings completed');
  }
};

const addEmbeddingsToRedis = async (
  _products: Prisma.ProductCreateInput[],
  _redisClient: NodeRedisClientType,
  _openAIApiKey: string,
  _huggingFaceApiKey?: string,
) => {
  if (_products?.length > 0 && _redisClient && _openAIApiKey) {
    const vectorDocs = await convertToVectorDocuments(_products);

    await seedOpenAIEmbeddings(vectorDocs, _redisClient, _openAIApiKey);
  }
};
TIP

Download RedisInsight to visually explore your Redis data or to engage with raw Redis commands in the workbench.

Setting up the search API#

API end point#

POST http://localhost:3000/products/getProductsByVSSText
{
   "searchText":"pure cotton blue shirts",

   //optional
   "maxProductCount": 4, // 2 (default)
   "similarityScoreLimit":0.2, // 0.2 (default)
}
{
  "data": [
    {
      "productId": "11031",
      "price": 1099,
      "productDisplayName": "Jealous 21 Women Check Blue Tops",
      "productDescriptors_description_value": "Composition : Green and navy blue checked round neck blouson tunic top made of 100% cotton, has a full buttoned placket, three fourth sleeves with buttoned cuffs and a belt below the waist<br /><br /><strong>Fitting</strong><br />Regular<br /><br /><strong>Wash care</strong><br />Machine/hand wash separately in mild detergent<br />Do not bleach or wring<br />Dry in shade<br />Medium iron<br /><br />If you're in the mood to have some checked fun, this blouson tunic top from jealous 21 will fulfil your heart's desire with &eacute;lan. The cotton fabric promises comfort, while the smart checks guarantee unparalleled attention. Pair this top with leggings and ballerinas for a cute, neat look.<br /><br /><em>Model statistics</em><br />The model wears size M in tops<br />Height: 5'7\"; Chest: 33\"; Waist: 25\"</p>",
      "stockQty": 25,
      "productColors": "Blue,Green",
      "similarityScore": 0.168704152107
      //...
    }
  ],
  "error": null,
  "auth": "SES_fd57d7f4-3deb-418f-9a95-6749cd06e348"
}

API implementation#

server/src/services/products/src/open-ai-prompt.ts
const getSimilarProductsScoreByVSS = async (
  _params: IParamsGetProductsByVSS,
) => {
  let {
    standAloneQuestion,
    openAIApiKey,

    //optional
    KNN,
    scoreLimit,
  } = _params;

  let vectorDocs: Document[] = [];
  const client = getNodeRedisClient();

  KNN = KNN || 2;
  scoreLimit = scoreLimit || 1;

  let embeddings = new OpenAIEmbeddings({
    openAIApiKey: openAIApiKey,
  });
  let indexName = 'openAIProductsIdx';
  let keyPrefix = 'openAIProducts:';

  if (embeddings) {
    // create vector store
    const vectorStore = new RedisVectorStore(embeddings, {
      redisClient: client,
      indexName: indexName,
      keyPrefix: keyPrefix,
    });

    // search for similar products
    const vectorDocsWithScore = await vectorStore.similaritySearchWithScore(
      standAloneQuestion,
      KNN,
    );

    // filter by scoreLimit
    for (let [doc, score] of vectorDocsWithScore) {
      if (score <= scoreLimit) {
        doc['similarityScore'] = score;
        vectorDocs.push(doc);
      }
    }
  }

  return vectorDocs;
};
server/src/services/products/src/service-impl.ts
const getProductsByVSSText = async (
  productsVSSFilter: IProductsVSSBodyFilter,
) => {
  let { searchText, maxProductCount, similarityScoreLimit } = productsVSSFilter;
  let products: IProduct[] = [];

  const openAIApiKey = process.env.OPEN_AI_API_KEY || '';
  maxProductCount = maxProductCount || 2;
  similarityScoreLimit = similarityScoreLimit || 0.2;

  if (!openAIApiKey) {
    throw new Error('Please provide openAI API key in .env file');
  }

  if (!searchText) {
    throw new Error('Please provide search text');
  }

  //VSS search
  const vectorDocs = await getSimilarProductsScoreByVSS({
    standAloneQuestion: searchText,
    openAIApiKey: openAIApiKey,
    KNN: maxProductCount,
    scoreLimit: similarityScoreLimit,
  });

  if (vectorDocs?.length) {
    const productIds = vectorDocs.map((doc) => doc?.metadata?.productId);

    //get product with details
    products = await getProductByIds(productIds, true);
  }

  //...

  return products;
};

Frontend UI#

Further reading#