diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java index 8c4ff0c2a9c..fb4a7796fec 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java @@ -70,4 +70,16 @@ default List similaritySearch(String query) { return this.similaritySearch(SearchRequest.query(query)); } + /** + * Retrieves documents by query embedding similarity using the default, If the + * subclass implements this method, use hybrid search, similar search + full text + * search. {@link SearchRequest}'s' search criteria. + * @param request request for set search parameters, such as the query text, topK, + * similarity threshold and metadata filter expressions. + * @return Returns a list of documents that have embeddings similar to the query text + */ + default List hybridSearch(SearchRequest request) { + return this.similaritySearch(request); + } + } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java index df5be884cf6..d0dfdea0e0a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java @@ -95,18 +95,23 @@ public void addAndSearch() { vectorStore.add(documents); - List results = vectorStore + List similarityResults = vectorStore .similaritySearch(SearchRequest.query("What is Great Depression?").withTopK(1)); + assertThat(similarityResults).hasSize(1); + Document similarityResultDoc = similarityResults.get(0); + assertThat(similarityResultDoc.getId()).isEqualTo(documents.get(2).getId()); + assertThat(similarityResultDoc.getMetadata()).containsKeys("depression", "distance"); - assertThat(results).hasSize(1); - Document resultDoc = results.get(0); - assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId()); - assertThat(resultDoc.getMetadata()).containsKeys("depression", "distance"); + List hybridResults = vectorStore + .hybridSearch(SearchRequest.query("What is Great Depression?").withTopK(1)); + assertThat(hybridResults).hasSize(2); + Document hybridResultDoc = hybridResults.get(1); + assertThat(hybridResultDoc.getMetadata()).containsKeys("depression"); // Remove all documents from the store vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList()); - results = vectorStore.similaritySearch(SearchRequest.query("Great Depression").withTopK(1)); - assertThat(results).hasSize(0); + similarityResults = vectorStore.similaritySearch(SearchRequest.query("Great Depression").withTopK(1)); + assertThat(similarityResults).hasSize(0); }); } diff --git a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java index bf2c662f716..b4e5b93f9f9 100644 --- a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java +++ b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java @@ -239,6 +239,21 @@ public List similaritySearch(SearchRequest request) { new DocumentRowMapper(this.objectMapper), queryEmbedding, queryEmbedding, distance, request.getTopK()); } + @Override + public List hybridSearch(SearchRequest searchRequest) { + + List similaritySearch = similaritySearch(searchRequest); + + String sql = "SELECT *, ts_rank_cd(to_tsvector(content), plainto_tsquery(?)) AS rank FROM " + + getFullyQualifiedTableName() + " ORDER BY rank DESC LIMIT ?"; + + List keyLikeSearch = this.jdbcTemplate.query(sql, new DocumentRowMapper(this.objectMapper, true), + searchRequest.getQuery(), searchRequest.getTopK()); + + similaritySearch.addAll(keyLikeSearch); + return similaritySearch; + } + public List embeddingDistance(String query) { return this.jdbcTemplate.query( "SELECT embedding " + this.comparisonOperator() + " ? AS distance FROM " + getFullyQualifiedTableName(), @@ -425,21 +440,28 @@ private static class DocumentRowMapper implements RowMapper { private ObjectMapper objectMapper; + private boolean isHybrid = false; + public DocumentRowMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } + public DocumentRowMapper(ObjectMapper objectMapper, boolean isHybrid) { + this.objectMapper = objectMapper; + this.isHybrid = isHybrid; + } + @Override public Document mapRow(ResultSet rs, int rowNum) throws SQLException { String id = rs.getString(COLUMN_ID); String content = rs.getString(COLUMN_CONTENT); PGobject pgMetadata = rs.getObject(COLUMN_METADATA, PGobject.class); PGobject embedding = rs.getObject(COLUMN_EMBEDDING, PGobject.class); - Float distance = rs.getFloat(COLUMN_DISTANCE); - Map metadata = toMap(pgMetadata); - metadata.put(COLUMN_DISTANCE, distance); - + if (!isHybrid) { + Float distance = rs.getFloat(COLUMN_DISTANCE); + metadata.put(COLUMN_DISTANCE, distance); + } Document document = new Document(id, content, metadata); document.setEmbedding(toDoubleList(embedding));