如何将Oracle vector数据库与Spring AI集成?

在本文中,我们探讨了如何将Oracle vector数据库与Spring AI集成。

我们介绍了必要的配置,并实现了两个关键的向量存储功能:相似性搜索和RAG。使用Testcontainers,我们设置Oracle vector数据库,创建本地测试环境。

首先,我们从Breaking Bad引用API中获取引用,以便在应用程序启动期间填充向量存储。然后,我们对存储的数据进行相似性搜索,以获取与系列中的共同主题相匹配的引用。

最后,我们实现了一个RAG聊天机器人,它使用从相似性搜索中检索到的引用作为上下文来回答用户查询。

对于传统的数据库,我们通常依赖于精确的关键字或基本模式匹配来实现搜索功能。虽然对于简单的应用程序来说足够了,但这种方法无法完全理解自然语言查询背后的含义和上下文。

向量存储通过将数据存储为捕获其含义的数字向量来解决此限制。相似的词被聚集在一起,这允许相似性搜索,其中数据库返回相关的结果,即使它们不包含查询中使用的确切关键字。

Oracle Database 23ai将这种矢量存储功能集成到其现有的生态系统中,使我们能够在不需要单独矢量存储的情况下构建AI应用程序。使用相同的数据库,我们可以创建使用传统结构化数据管理和向量相似性搜索的解决方案。

在本教程中,我们将探索如何将Oracle vector数据库与Spring AI集成。我们将实现本地相似性搜索来查找语义相关的内容。然后,我们将在此功能的基础上实现检索增强生成(RAG)聊天机器人。



设置项目
在我们深入实现之前,我们需要包含必要的依赖项并正确配置我们的应用程序。


1.依赖关系
让我们首先向项目的pom.xml文件添加必要的依赖项:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-oracle</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>1.0.0</version>
</dependency>

Oracle vector store starter依赖项使我们能够与Oracle vector数据库建立连接并与之交互。此外,我们还为RAG实施导入vector store advisors依赖项。

最后,我们导入Spring AI的OpenAI starter依赖项,我们将使用它与聊天完成和嵌入模型进行交互。

鉴于我们在项目中使用了多个Spring AI starter,让我们在pom.xml中包含Spring AI Bill of Materials(BOM):

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

有了这个添加,我们现在可以从我们的starter依赖项中删除version标签。BOM消除了版本冲突的风险,并确保Spring AI依赖项相互兼容。

2.配置AI模型和向量存储属性
要将文本数据转换为Oracle vector数据库可以存储和搜索的向量,我们需要一个嵌入模型。此外,对于我们的RAG聊天机器人,我们还需要一个聊天完成模型。

在我们的演示中,我们将使用OpenAI提供的模型。让我们在application.yaml文件中配置OpenAI API密钥和模型:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      embedding:
        options:
          model: text-embedding-3-large
      chat:
        options:
          model: gpt-4o

我们使用${}属性占位符从环境变量加载API键的值。

此外,我们指定text-embedding-3-large和gpt-4 o分别作为我们的嵌入和聊天完成模型。在配置这些属性时,Spring AI会自动创建一个ChatModel类型的bean,我们将在本教程的后面部分使用它。

或者,我们可以使用不同的模型,因为特定的AI模型或提供商与此演示无关。

接下来,要在我们的向量数据库中存储和搜索数据,我们必须首先初始化它的模式:

spring:
  ai:
    vectorstore:
      oracle:
        initialize-schema: true

这里,我们将spring.ai.vectorstore.oracle.initialize-schema设置为true。

这指示Spring AI在应用程序启动时自动创建必要的默认向量存储模式,这便于本地开发和测试。但是,对于生产应用程序,我们应该使用数据库迁移工具(如Flyway)手动定义模式。



填充Oracle Vector数据库
配置就绪后,让我们设置一个工作流,在应用程序启动期间使用一些示例数据填充Oracle向量数据库。

1.获取引用来自外部API的记录
对于我们的演示,我们将使用Breaking Bad Quotes API来获取报价。

让我们为此创建一个QuoteFetcher实用程序类:

class QuoteFetcher {
    private static final String BASE_URL = "https://api.breakingbadquotes.xyz/v1/quotes/";
    private static final int DEFAULT_COUNT = 150;
    static List<Quote> fetch() {
        return fetch(DEFAULT_COUNT);
    }
    static List<Quote> fetch(int count) {
        return RestClient
          .create()
          .get()
          .uri(URI.create(BASE_URL + count))
          .retrieve()
          .body(new ParameterizedTypeReference<>() {});
    }
}
record Quote(String quote, String author) {
}

通过使用RestClient,我们调用默认计数为150的外部API,并使用ParameterizedTypeReference将API响应转换为Quote记录列表。

2.存储文件矢量数据库
现在,为了在应用程序启动期间使用引号填充Oracle vector数据库,我们将创建一个实现ApplicationRunner接口的VectorStoreInitializer类:

@Component
class VectorStoreInitializer implements ApplicationRunner {
    private final VectorStore vectorStore;
    // standard constructor
    @Override
    public void run(ApplicationArguments args) {
        List<Document> documents = QuoteFetcher
          .fetch()
          .stream()
          .map(quote -> {
              Map<String, Object> metadata = Map.of(
"author", quote.author());
              return new Document(quote.quote(), metadata);
          })
          .toList();
        vectorStore.add(documents);
    }
}

在我们的VectorStoreInitializer类中,我们自动连接VectorStore的一个实例,Spring AI会自动为我们创建它。

在run()方法中,我们使用QuoteFetcher实用程序类检索Quote记录列表。然后,我们将每个引用映射到一个Document中,并将author字段配置为元数据。

最后,我们将所有文档存储在数据库中。当我们调用add()方法时,Spring AI会自动将我们的明文内容转换为矢量表示,然后将其存储到数据库中。



使用Testcontainers建立本地测试环境
为了方便本地开发和测试,我们将使用Testcontainers来设置Oracle vector数据库,其先决条件是一个活动的Docker实例。

首先,让我们将必要的测试依赖项添加到pom.xml:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>oracle-free</artifactId>
    <scope>test</scope>
</dependency>

我们为Spring Boot导入Spring AI Testcontainers依赖项和Testcontainers的Oracle数据库模块。靴子。

这些依赖项提供了必要的类来为Oracle vector数据库启动一个临时Docker实例。

接下来,让我们创建一个@TestConfiguration类来定义我们的Testcontainers bean:

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
    @Bean
    @ServiceConnection
    OracleContainer oracleContainer() {
        return new OracleContainer("gvenzl/oracle-free:23-slim");
    }
}

我们在创建OracleContainer bean时指定Oracle数据库映像的最新稳定精简版本。

此外,我们用@ServiceConnection注释我们的bean方法。这将动态注册与Docker容器建立连接所需的所有Docker属性。

现在,我们可以在集成测试中使用此配置,方法是使用@Import(Testcontainersconfiguration.class)注释来注释测试类。



执行相似性分析
现在,我们已经设置了本地测试环境,并使用Breaking Bad引用填充了Oracle向量数据库,接下来让我们探索如何执行相似性搜索。

1.基本相似性搜索
让我们从执行基本的相似性搜索操作开始,以查找与各种Breaking Bad主题匹配的引用:

private static final int MAX_RESULTS = 5;
@Autowired
private VectorStore vectorStore;
@ParameterizedTest
@ValueSource(strings = { "Sarcasm", "Regret", "Violence and Threats", "Greed, Power, and Money" })
void whenSearchingBreakingBadTheme_thenRelevantQuotesReturned(String theme) {
    SearchRequest searchRequest = SearchRequest
      .builder()
      .query(theme)
      .topK(MAX_RESULTS)
      .build();
    List<Document> documents = vectorStore.similaritySearch(searchRequest);
    assertThat(documents)
      .hasSizeGreaterThan(0)
      .hasSizeLessThanOrEqualTo(MAX_RESULTS)
      .allSatisfy(document -> {
          assertThat(document.getText())
            .isNotBlank();
          assertThat(String.valueOf(document.getMetadata().get(
"author")))
            .isNotBlank();
      });
}

在这里,我们使用@ValueSource将Breaking Bad系列的主题传递给我们的测试方法。然后,我们创建一个SearchRequest对象,主题作为查询。此外,我们通过将MAX_RESULTS传递给topK()方法,将结果限制为前五个最相似的引号。

接下来,我们使用searchRequest调用vectorStore bean的similaritySearch()方法。与VectorStore的add()方法类似,Spring AI在查询数据库之前将我们的查询转换为它的向量表示。

返回的文档将包含与给定主题语义相关的引号,即使它们不包含确切的关键字。

2.使用元数据过滤
除了执行基本的相似性搜索外,Oracle矢量数据库还支持根据保存的元数据过滤搜索结果。当我们需要缩小搜索范围并在数据子集内执行语义搜索时,这很有用。

让我们再次搜索与给定主题相关的引用,但按特定作者过滤它们:

@ParameterizedTest
@CsvSource({
    "Walter White, Pride",
   
"Walter White, Control",
   
"Jesse Pinkman, Abuse and foul language",
   
"Mike Ehrmantraut, Wisdom",
   
"Saul Goodman, Law"
})
void whenSearchingCharacterTheme_thenRelevantQuotesReturned(String author, String theme) {
    SearchRequest searchRequest = SearchRequest
      .builder()
      .query(theme)
      .topK(MAX_RESULTS)
      .filterExpression(String.format(
"author == '%s'", author))
      .build();
    List<Document> documents = vectorStore.similaritySearch(searchRequest);
    assertThat(documents)
      .hasSizeGreaterThan(0)
      .hasSizeLessThanOrEqualTo(MAX_RESULTS)
      .allSatisfy(document -> {
          assertThat(document.getText())
            .isNotBlank();
          assertThat(String.valueOf(document.getMetadata().get(
"author")))
            .contains(author);
      });
}

在这里,我们使用@CsvSource注释来查找使用各种字符主题组合的引用。

我们像以前一样构建SearchRequest,但这一次,我们使用filterExpression()方法将结果限制为来自特定作者的引用。



创建RAG Chatbot
虽然原生相似性搜索本身就很强大,但我们可以在此基础上创建一个智能的、上下文感知的RAG聊天机器人。

1.定义提示模板
为了更好地指导LLM的行为,我们将定义一个自定义提示模板。让我们在src/main/resources目录中创建一个新的prompt-template.st文件:

You are a chatbot built for analyzing quotes from the 'Breaking Bad' television series.
Given the quotes in the CONTEXT section, answer the query in the USER_QUESTION section.
The response should follow the guidelines listed in the GUIDELINES section.
CONTEXT:
<question_answer_context>
USER_QUESTION:
<query>
GUIDELINES:
- Base your answer solely on the information found in the provided quotes.
- Provide concise, direct answers without mentioning "based on the context" or similar phrases.
- When referencing specific quotes, mention the character who said them.
- If the question cannot be answered using the context, respond with
"The provided quotes do not contain information to answer this question."
- If the question is unrelated to the Breaking Bad show or the quotes provided, respond with
"This question is outside the scope of the available Breaking Bad quotes."

在这里,我们清楚地定义了聊天机器人的角色,并为它提供了一套遵循的指导方针。

在我们的模板中,我们使用两个用尖括号括起来的占位符。Spring AI将自动分别用从vector数据库和用户问题中检索到的上下文替换question_answer_context和query占位符。

2.配置聊天客户端豆
接下来,让我们定义一个ChatClient类型的bean,它充当与配置的聊天完成模型交互的主入口点:

private static final int MAX_RESULTS = 10;
@Bean
PromptTemplate promptTemplate(
  @Value("classpath:system-prompt.st") Resource promptTemplate) {
    String template = promptTemplate.getContentAsString(StandardCharsets.UTF_8);
    return PromptTemplate
      .builder()
      .renderer(StTemplateRenderer
        .builder()
        .startDelimiterToken('<')
        .endDelimiterToken('>')
        .build())
      .template(template)
      .build();
}
@Bean
ChatClient chatClient(
  ChatModel chatModel,
  VectorStore vectorStore,
  PromptTemplate promptTemplate) {
    return ChatClient
      .builder(chatModel)
      .defaultAdvisors(
        QuestionAnswerAdvisor
          .builder(vectorStore)
          .promptTemplate(promptTemplate)
          .searchRequest(SearchRequest
            .builder()
            .topK(MAX_RESULTS)
            .build())
          .build()
      )
      .build();
}

在这里,我们首先使用@Value注释检索prompt模板的内容,并使用它定义一个prompt模板bean。我们还将其配置为使用尖括号作为分隔符。

接下来,我们使用ChatTemplate bean,沿着ChatModel和VectorStore bean,来定义ChatClient bean。我们使用defaultAdvisors()方法来注册一个XMLServerAdvisor,它是实现RAG模式的组件。

此外,在顾问中,我们配置了一个SearchRequest来检索前10个最相关的报价。Spring AI将在调用LLM之前将它们注入到prompt模板中。

3.执行RAG操作
现在,配置好ChatClient bean后,让我们看看如何与它交互以询问自然语言问题:

@Autowired
private ChatClient chatClient;
@ParameterizedTest
@ValueSource(strings = {
    "How does the show portray the mentor-student dynamic?",
   
"Which characters in the show portray insecurity through their quotes?",
   
"Does the show contain quotes with mature themes inappropriate for young viewers?"
})
void whenQuestionsRelatedToBreakingBadAsked_thenRelevantAnswerReturned(String userQuery) {
    String response = chatClient
      .prompt(userQuery)
      .call()
      .content();
    assertThat(response)
      .isNotBlank();
      .doesNotContain(OUT_OF_SCOPE_MESSAGE, NO_INFORMATION_MESSAGE);
}

在这里,当我们将userQuery传递给prompt()方法时,我们配置的DataQuery Advisor在后台执行RAG工作流。advisor查询Oracle vector数据库中与用户问题相关的引用,将它们注入到提示模板中,并将组合的提示发送到配置的LLM以获得响应。

我们验证响应不是空的,并且不包含我们在模板中定义的回退消息。