Support similarity scores in Document API
Document * Introduced “score” attribute in Document API. It stores the similarity score. * Consolidate “distance” metadata for Documents. It stores the distance measurement. * Adopted prefix-less naming convention in Document.Builder and deprecated old methods. * Deprecated the many overloaded Document constructors in favour of Document.Builder. Vector Stores * Every vector store implementation now configures a “score” attribute with the similarity score of the Document embedding. It also includes the “distance” metadata with the distance measurement. * Fixed error in Elasticsearch where distance and similarity were mixed up. * Added missing integration tests for SimpleVectorStore. * The Azure Vector Store and HanaDB Vector Store do not include those measurements because the product documentation do not include information about how the similarity score is returned, and without access to the cloud products I could not verify that via debugging. * Improved tests to actually assert the result of the similarity search based on the returned score. Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
committed by
Mark Pollack
parent
50223d20e3
commit
fe58fd30eb
@@ -35,6 +35,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.document.DocumentMetadata;
|
||||
import org.springframework.ai.embedding.BatchingStrategy;
|
||||
import org.springframework.ai.embedding.EmbeddingModel;
|
||||
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
|
||||
@@ -502,12 +503,15 @@ public class PgVectorStore extends AbstractObservationVectorStore implements Ini
|
||||
Float distance = rs.getFloat(COLUMN_DISTANCE);
|
||||
|
||||
Map<String, Object> metadata = toMap(pgMetadata);
|
||||
metadata.put(COLUMN_DISTANCE, distance);
|
||||
metadata.put(DocumentMetadata.DISTANCE.value(), distance);
|
||||
|
||||
Document document = new Document(id, content, metadata);
|
||||
document.setEmbedding(toFloatArray(embedding));
|
||||
|
||||
return document;
|
||||
return Document.builder()
|
||||
.id(id)
|
||||
.content(content)
|
||||
.metadata(metadata)
|
||||
.score(1.0 - distance)
|
||||
.embedding(toFloatArray(embedding))
|
||||
.build();
|
||||
}
|
||||
|
||||
private float[] toFloatArray(PGobject embedding) throws SQLException {
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.ai.document.DocumentMetadata;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
@@ -113,20 +114,20 @@ public class PgVectorStoreIT {
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean isSortedByDistance(List<Document> docs) {
|
||||
private static boolean isSortedBySimilarity(List<Document> docs) {
|
||||
|
||||
List<Float> distances = docs.stream().map(doc -> (Float) doc.getMetadata().get("distance")).toList();
|
||||
List<Double> scores = docs.stream().map(Document::getScore).toList();
|
||||
|
||||
if (CollectionUtils.isEmpty(distances) || distances.size() == 1) {
|
||||
if (CollectionUtils.isEmpty(scores) || scores.size() == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Iterator<Float> iter = distances.iterator();
|
||||
Float current;
|
||||
Float previous = iter.next();
|
||||
Iterator<Double> iter = scores.iterator();
|
||||
Double current;
|
||||
Double previous = iter.next();
|
||||
while (iter.hasNext()) {
|
||||
current = iter.next();
|
||||
if (previous > current) {
|
||||
if (previous < current) {
|
||||
return false;
|
||||
}
|
||||
previous = current;
|
||||
@@ -150,7 +151,7 @@ public class PgVectorStoreIT {
|
||||
assertThat(results).hasSize(1);
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(this.documents.get(2).getId());
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta2", "distance");
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta2", DocumentMetadata.DISTANCE.value());
|
||||
|
||||
// Remove all documents from the store
|
||||
vectorStore.delete(this.documents.stream().map(doc -> doc.getId()).toList());
|
||||
@@ -289,7 +290,7 @@ public class PgVectorStoreIT {
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(document.getId());
|
||||
assertThat(resultDoc.getContent()).isEqualTo("Spring AI rocks!!");
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta1", "distance");
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta1", DocumentMetadata.DISTANCE.value());
|
||||
|
||||
Document sameIdDocument = new Document(document.getId(),
|
||||
"The World is Big and Salvation Lurks Around the Corner",
|
||||
@@ -303,7 +304,7 @@ public class PgVectorStoreIT {
|
||||
resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(document.getId());
|
||||
assertThat(resultDoc.getContent()).isEqualTo("The World is Big and Salvation Lurks Around the Corner");
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta2", "distance");
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta2", DocumentMetadata.DISTANCE.value());
|
||||
|
||||
dropTable(context);
|
||||
});
|
||||
@@ -326,20 +327,19 @@ public class PgVectorStoreIT {
|
||||
|
||||
assertThat(fullResult).hasSize(3);
|
||||
|
||||
assertThat(isSortedByDistance(fullResult)).isTrue();
|
||||
assertThat(isSortedBySimilarity(fullResult)).isTrue();
|
||||
|
||||
List<Float> distances = fullResult.stream()
|
||||
.map(doc -> (Float) doc.getMetadata().get("distance"))
|
||||
.toList();
|
||||
List<Double> scores = fullResult.stream().map(Document::getScore).toList();
|
||||
|
||||
float threshold = (distances.get(0) + distances.get(1)) / 2;
|
||||
double similarityThreshold = (scores.get(0) + scores.get(1)) / 2;
|
||||
|
||||
List<Document> results = vectorStore.similaritySearch(
|
||||
SearchRequest.query("Time Shelter").withTopK(5).withSimilarityThreshold(1 - threshold));
|
||||
SearchRequest.query("Time Shelter").withTopK(5).withSimilarityThreshold(similarityThreshold));
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(this.documents.get(1).getId());
|
||||
assertThat(resultDoc.getScore()).isGreaterThanOrEqualTo(similarityThreshold);
|
||||
|
||||
dropTable(context);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user