Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Upstash와 Node.js로 실시간 기사 추천 엔진 구축하기

구글이나 Perplexity.ai를 사용해 본 적이 있으신가요? 어떻게 최신 검색 결과를 보여주고 온라인 기사 링크까지 함께 제공할 수 있는지 궁금하신 적이 있으신가요? 이 가이드에서는 그러한 시스템을 직접 구축하는 방법을 단계별로 배워봅니다. 추가하는 기사 링크들이 점점 커지는 지식 저장소에 축적되고, 그 지식을 기반으로 개인화된 추천 결과를 생성하는 시스템을 만들어 보겠습니다.

사전 준비 사항

이 가이드를 따라 하려면 다음 항목들이 필요합니다.

  • Node.js 18 이상
  • Upstash 계정
  • OpenAI 계정
  • Fly.io 계정

기술 스택

기술설명
Upstash서버리스 데이터베이스 플랫폼. 벡터 임베딩과 메타데이터 저장을 위해 Upstash Vector를 사용합니다.
Remix웹 표준에 중점을 두고 풀스택 웹 애플리케이션을 구축하는 프레임워크입니다.
OpenAI첨단 AI 기술 개발에 집중하는 인공지능 연구 기업입니다.
LangChain언어 모델 기반 애플리케이션 개발을 위한 프레임워크입니다.
Vercel AI SDKAI 기반 사용자 인터페이스 구축을 위한 오픈소스 라이브러리입니다.
TailwindCSS커스텀 디자인 구축을 위한 CSS 프레임워크입니다.
Fly.io풀스택 앱과 데이터베이스를 사용자 근처에서 실행할 수 있는 플랫폼입니다.
Prettier일관된 코드 스타일을 유지하는 코드 포맷터입니다.

진행 순서

이 가이드를 완료하고 자신만의 기사 추천 시스템을 배포하려면 다음 단계를 따르세요.

  • OpenAI 토큰 생성하기
  • Upstash Vector 인덱스 생성하기
  • 프로젝트 설정하기
  • OpenAI API 클라이언트 인스턴스 생성하기
  • OpenAI API 임베딩 클라이언트 만들기
  • Upstash Vector 클라이언트 만들기
  • 컨텍스트(Context) API 엔드포인트 만들기
  • 채팅(Chat) API 엔드포인트 만들기
  • Fly.io에 배포하기
  • 참고 자료
  • 마무리

OpenAI 토큰 생성하기

OpenAI API를 사용하면 기사의 벡터 임베딩을 얻을 수 있고, AI를 활용한 챗봇 응답도 생성할 수 있습니다. OpenAI API에 대한 모든 요청에는 인증 토큰이 필요합니다. 토큰을 발급받으려면 OpenAI 계정의 API Keys 페이지로 이동한 후 Create new secret key 버튼을 클릭하세요. 발급된 토큰을 복사하여 안전하게 보관하고, 나중에 OPENAI_API_KEY 환경 변수로 사용하시면 됩니다.

Upstash Vector 인덱스 생성하기

Upstash 계정을 생성하고 로그인한 후, Vector 탭으로 이동하여 Create Index 버튼을 클릭해 벡터 인덱스 생성을 시작합니다.

원하는 인덱스 이름(예: article)을 입력하고 벡터 차원은 1536으로 설정합니다.

그다음 아래로 스크롤하여 Connect 섹션으로 이동한 뒤 .env 버튼을 클릭하세요. 내용을 복사해서 안전한 곳에 저장해 두면, 이후 애플리케이션에서 활용할 수 있습니다.

프로젝트 설정하기

설정을 시작하려면 앱 저장소를 클론하고, 이 가이드를 따라가며 코드의 모든 내용을 학습하세요. 프로젝트를 클론하려면 터미널에서 다음 명령어를 실행합니다.

# Clone the project
git clone https://github.com/rishi-raj-jain/article-recommendation-system
cd article-recommendation-system
 
# Install the dependencies
pnpm install

저장소를 클론한 후 .env 파일을 생성하고, 앞서 발급받은 시크릿 키들을 추가합니다.

.env 파일에는 다음과 같은 키들이 포함되어야 합니다.

# .env
 
# OpenAI API Key
OPENAI_API_KEY="sk-..."
 
# Upstash Vector Keys
UPSTASH_VECTOR_REST_URL="https://...-us1-vector.upstash.io"
UPSTASH_VECTOR_REST_TOKEN="...="

여기까지 완료되면 설정 작업은 끝납니다. 이제 터미널에서 아래 명령어를 실행하고 localhost:3000에 접속하면 애플리케이션이 실제로 동작하는 모습을 확인할 수 있습니다.

pnpm run build && pnpm run start

계속 진행하며 자신만의 기사 추천 시스템을 성공적으로 구축할 수 있게 해주는 핵심 코드들을 살펴보겠습니다.

OpenAI API 클라이언트 인스턴스 생성하기

openai 패키지를 사용하면 몇 줄의 코드만으로 OpenAI REST API와 손쉽게 상호작용할 수 있습니다. 아래 코드로 OpenAI API 클라이언트 라이브러리의 인스턴스를 생성하여, 이후 채팅 완성(chat completion) 응답 생성에 활용합니다.

// File: app/lib/openai/completion.server.ts
 
import OpenAI from 'openai'
 
// Instantiate class to generate text completion using the OpenAI API
export default new OpenAI({
 apiKey: process.env.OPENAI_API_KEY,
})

참고: Remix에서 파일 이름 끝에 .server.ts를 붙이면 해당 코드가 클라이언트 사이드 번들에서 확실히 제외됩니다.

OpenAI API 임베딩 클라이언트 만들기

@langchain/openai 패키지를 사용하면 OpenAIEmbeddings 클래스로 주어진 텍스트의 벡터 임베딩을 생성할 수 있습니다. 이 클래스를 LangChain 벡터 스토어와 함께 사용하면, 각 벡터 임베딩을 직접 만들고 삽입하는 번거로운 과정을 생략할 수 있습니다. 아래 코드로 OpenAIEmbeddings 클래스의 인스턴스를 생성하여, 이후 임베딩 생성을 내부적으로 처리하도록 합니다.

// File: app/lib/openai/embedding.server.ts
 
import { OpenAIEmbeddings } from '@langchain/openai'
 
// Instantiate class to generate embeddings using the OpenAI API
export default new OpenAIEmbeddings({
 modelName: 'text-embedding-3-small',
 openAIApiKey: process.env.OPENAI_API_KEY,
})

Upstash Vector 클라이언트 만들기

@upstash/vector@langchain/community/vectorstores/upstash 패키지를 사용하면 Remix 애플리케이션에서 커넥션리스(connectionless) 클라이언트를 만들어, Upstash Vector 인덱스에 벡터 임베딩을 저장·삭제·조회할 수 있습니다.

// File: app/lib/upstash/vectorStore.server.ts
 
import embeddings from '~/lib/openai/embedding.server'
import { Index as UpstashIndex } from '@upstash/vector'
import { UpstashVectorStore } from '@langchain/community/vectorstores/upstash'
 
// Instantiate the Upstash Vector Index
const index = new UpstashIndex({
 url: process.env.UPSTASH_VECTOR_REST_URL as string,
 token: process.env.UPSTASH_VECTOR_REST_TOKEN as string,
})
 
// Instantiate the Upstash Vector Store that'll create and save embeddings
export default new UpstashVectorStore(embeddings, { index })

컨텍스트(Context) API 엔드포인트 만들기

Remix 애플리케이션을 실행하면 여러 개의 기사 URL을 입력받는 텍스트 박스가 표시됩니다. 이 기사들은 챗봇의 지식으로 추가되어, 이후 사용자 검색 시 개인화된 응답을 생성하는 데 활용됩니다. 이 섹션에서는 컨텍스트 엔드포인트(app/routes/api_.context.tsx)가 여러 기사 URL을 받아 콘텐츠를 가져오고, 벡터 임베딩을 생성한 후, 이를 동적으로 Upstash Vector 인덱스에 저장하는 방식을 살펴봅니다.

// File: app/routes/api_.context.tsx
 
import { Document } from 'langchain/document'
import { ActionFunctionArgs } from '@remix-run/node'
import vectorServer from '~/lib/vector/vectorStore.server'
import { CheerioWebBaseLoader } from 'langchain/document_loaders/web/cheerio'
 
export const action = async ({ request }: ActionFunctionArgs) => {
 const formData = await request.formData()
 // Check if any article link are present in the form submission
 const articlesToEmbed = formData.get('articles') as string
 if (articlesToEmbed) {
 // Create the documents to be added to the Upstash Vector Store
 const documents: any[] = []
 await Promise.all(
 articlesToEmbed.split(',').map(async (link) => {
 // Use the link to render in the search results
 // Parse the link using Cheerio
 const loader = new CheerioWebBaseLoader(link.trim())
 const scraper = await loader.scrape()
 // Get the content of title tag to render in the search results
 const name = scraper('title').html()
 // Get the page content as string
 const pageContent = scraper.text()
 // Create metadata object to be inserted in the vector store
 const metadata = { link, name }
 documents.push(new Document({ pageContent, metadata }))
 }),
 )
 // Creating embeddings from the provided documents along with metadata
 // and add them to Upstash database
 await vectorServer.addDocuments(documents.filter(Boolean))
 }
}

위 Remix 액션에서는 컨텍스트 엔드포인트(/api/context)로 전송된 POST 요청의 폼 데이터를 파싱합니다. 그런 다음 콤마(,)로 구분된 기사 링크 목록을 순회하면서 다음 작업을 수행합니다.

  • 기사 웹페이지에서 가져온 텍스트 콘텐츠로 pageContent 변수를 생성합니다.
  • 기사 웹페이지의 제목으로 name 변수를 생성합니다.
  • 텍스트 콘텐츠, 참조 링크, 기사 이름을 담은 LangChain Document를 생성합니다(new Document({ pageContent, metadata })).
  • 각 문서를 전역 documents 배열에 추가합니다.

마지막으로 전역 documents 배열에 저장된 모든 문서가 Upstash Vector 인덱스에 삽입됩니다. 이때 각 문서의 벡터 임베딩은 해당 문서의 pageContent 속성을 기반으로 자동 생성됩니다.

채팅(Chat) API 엔드포인트 만들기

이 섹션에서는 채팅 API 엔드포인트(app/routes/api_.chat.tsx)가 사용자 검색과 관련된 기사를 추천하는 검색 엔진 스타일의 응답을 생성하도록 구성하는 방법을 알아봅니다. 검색과 관련된 기사는 특정 벡터 인덱스에서 가장 가까운 top-K 벡터를 찾아 식별됩니다. 이후 해당 벡터들의 메타데이터에 있는 제목과 링크가 컨텍스트로 OpenAI API에 전달됩니다. 덕분에 챗봇은 사용자 검색에 응답하면서 관련 기사를 외부 링크로 함께 제공할 수 있습니다. 설명을 위해 몇 부분으로 나누어 살펴보겠습니다.

유사도 검색으로 관련 벡터 임베딩 찾기

매번 사용자 검색마다 기사 지식 저장소 전체를 다시 훑어보는 것은 비용이 큰 작업입니다. 사용자 검색과 (있다면) 매우 관련성이 높은 상위 3개 기사로 범위를 좁히려면, Upstash Vector 인덱스에 있는 기존 벡터들을 조회하세요. 그리고 사용자 검색의 벡터 임베딩과 최소 70% 이상의 유사도 점수를 가진 벡터만 필터링하여 남깁니다.

// File: app/routes/api_.chat.tsx
 
import vectorServer from '~/lib/upstash/vectorStore.server'
import type { ActionFunctionArgs } from '@remix-run/node'
 
export const action = async ({ request }: ActionFunctionArgs) => {
 
 // Set of messages between user and chatbot
 const { messages = [] } = await request.json()
 
 // Get the latest question stored in the last message of the chat array
 const searchQuery = messages[messages.length - 1].content
 
 // Perform Similarity Search using the Upstash Vector Store
 const queryResult = await vectorServer.similaritySearchWithScore(searchQuery, 3)
 
 // Filter the records with confidence score > 70% and
 // set the metadata as response to render search results
 const results = queryResult.filter((i) => i[1] >= 0.7).map((i) => i[0].metadata)
 
 // Proceed to create a response
}

챗봇을 위한 시스템 컨텍스트와 지침 만들기

이제 관련성 높은 벡터들을 확보했으니, 챗봇이 사용자 검색에 응답하기 전에 해당 기사들을 인지하고 참조하도록 만들어야 합니다. OpenAI의 gpt-3.5-turbo 모델로 이를 구현하려면 role 속성을 system으로 설정한 메시지 객체를 만들고, content 속성에 다음 지침들을 포함시킵니다.

  • 챗봇은 구글처럼 응답해야 합니다.
  • 응답은 반드시 마크다운(markdown) 형식이어야 합니다.
  • 응답에는 기사로 연결되는 하이퍼링크가 포함되어야 합니다.
  • 챗봇은 단순히 기사 참조를 넘어 더 유용한 정보를 제공해야 합니다.
// File: app/routes/api_.train.tsx
 
import { OpenAIStream, StreamingTextResponse } from 'ai'
import completionServer from '~/lib/openai/completion.server'
 
export const action = async ({ request }: ActionFunctionArgs) => {
 // ...
 
 // Now use OpenAI Text Completion with relevant articles as context
 const completionResponse = await completionServer.chat.completions.create({
 stream: true,
 model: 'gpt-3.5-turbo',
 messages: [
 {
 // create a system content message to be added as
 // the open ai text completion will supply it as the context with the API
 role: 'system',
 content: `Behave like a Google. You have the knowledge of the following articles: ${JSON.stringify(results)}. Each response should be in 100% markdown compatible format and should have hyperlinks in it. Be precise. Do add some general text in the response related to the query.`,
 },
 // also, pass the whole conversation!
 ...messages,
 ],
 })
 
 // Convert the response into a friendly text-stream
 const stream = OpenAIStream(completionResponse)
 // Respond with the stream
 return new StreamingTextResponse(stream)
}
 

위 코드를 통해 사용자 검색과 관련성이 높다고 판단된 기사를 추천하는, 컨텍스트를 인식하는 OpenAI 스트리밍 응답을 구현할 수 있습니다.

배울 내용이 정말 많았습니다! 이제 모든 준비가 끝났습니다 ✨

Fly.io에 배포하기

이 저장소에는 Fly.io 배포를 위한 설정이 미리 포함되어 있습니다. 구체적으로 다음 파일들입니다.

  • Dockerfile
  • fly.toml
  • .dockerignore

Fly.io 계정이 있다면, 프로젝트 루트 디렉터리에서 터미널에 다음 명령어를 실행해 Fly.io에 앱을 생성할 수 있습니다.

# Create an app based on the baked-in configuration in your account
# This will result only in the change of app name in existing fly.toml
 
fly launch

그리고 다음 명령어를 실행하여 Fly.io에 배포합니다.

# Deploy the app based on the configuration created above
 
fly deploy

참고 자료

더 자세한 내용이 궁금하다면 이 가이드에서 활용한 아래 자료들을 살펴보세요.

  • GitHub 저장소
  • LangChain과 Upstash Vector Store 통합
  • OpenAI Chat Completions API의 시스템 지침(System Instructions)
  • LangChain에서 Cheerio로 웹페이지 데이터 불러오기
  • React 앱에서 AI 채팅 UI 만들기

마무리

이 가이드에서는 벡터 임베딩과 OpenAI Completion API를 활용하고, 동적으로 생성된 시스템 컨텍스트를 결합하여 기사 추천 시스템을 구축하는 방법을 배웠습니다. Upstash Vector와 LangChain을 사용하면 벡터를 인덱스에 저장하고, top-K 벡터 검색 쿼리를 수행하며, 사용자 검색마다 관련 컨텍스트를 생성하는 모든 작업을 몇 줄의 코드로 처리할 수 있습니다.

질문이나 의견이 있다면 GitHub를 통해 언제든지 연락해 주세요.