BM25-Funktion
Die BM25-Funktion ermöglicht eine Volltextsuche, indem sie Rohtext in spärliche Vektoren umwandelt und Dokumente auf der Grundlage lexikalischer Relevanz bewertet. Sie wendet termbasiertes Matching und frequenzabhängige Gewichtung an, um eine effiziente Suche nach Textdokumenten zu unterstützen, die eng mit den Suchbegriffen übereinstimmen.
Als lokale Textfunktion läuft die BM25-Funktion innerhalb von Milvus und erfordert keine Modellinferenz oder externe Integrationen. Sie bietet einen deterministischen und transparenten Abrufmechanismus für textbasierte Suchszenarien.
Wie funktioniert BM25?
Der BM25-Algorithmus ist ein begriffsbasierter Relevanzbewertungsalgorithmus, der in der Volltextsuche weit verbreitet ist. In Milvus ist BM25 als Sparse-Retrieval-Pipeline implementiert, die Text in begriffsgewichtete Repräsentationen umwandelt und die Top-K-Dokumente mithilfe verteilter Sparse-Indizes abruft.
Der gesamte Arbeitsablauf besteht aus zwei symmetrischen Pfaden: Dokumenteneingabe und Textverarbeitung, die dieselbe Textanalyselogik nutzen.
Einlesen von Dokumenten: Vom Text zur spärlichen Darstellung
Wenn ein Dokument eingefügt wird, wird sein Rohtext zunächst von einem Analysator verarbeitet, der den Text in einzelne Begriffe aufschlüsselt.
Zum Beispiel kann das Dokument:
"We are loving Milvus!"
kann in die folgenden Begriffe zerlegt werden:
["we", "love", "milvus"]
Jedes Dokument wird dann als Termfrequenzdarstellung (TF) dargestellt, die aufzeichnet, wie oft jeder Begriff in dem Dokument vorkommt. Ein Beispiel:
{
"we": 1,
"love": 1,
"milvus": 1
}
Gleichzeitig aktualisiert Milvus Statistiken auf Korpusebene, darunter:
die Dokumenthäufigkeit (DF) eines jeden Begriffs
die durchschnittliche Dokumentlänge
Buchungslisten, die jeden Begriff den Dokumenten zuordnen, die ihn enthalten
Die TF-Darstellung des Dokuments wird in Sparse Embeddings eingefügt, in denen die Termpostings für eine skalierbare Abfrage auf die Knoten verteilt werden.
Textabfrage-Prozess: IDF-Gewichtung anwenden
Wenn eine textbasierte Abfrage gestellt wird, wird sie von demselben Analysator verarbeitet, der auch bei der Aufnahme von Dokumenten verwendet wird, um eine konsistente Termsegmentierung sicherzustellen.
Ein Beispiel: Die Abfrage:
"who loves Milvus?"
kann analysiert werden in:
["who", "love", "milvus"]
Für jeden Abfrageterminus ermittelt Milvus seine inverse Dokumentenhäufigkeit (IDF) aus den Korpusstatistiken. Die IDF spiegelt wider, wie informativ ein Begriff im gesamten Datensatz ist: Seltene Begriffe erhalten eine höhere Gewichtung, während häufige Begriffe eine geringere Gewichtung erhalten.
Daraus ergibt sich eine Reihe von IDF-gewichteten Suchbegriffen, wie z. B.:
{
"who": 0.1,
"love": 0.5,
"milvus": 1.2
}
BM25-Bewertung und Top-K-Retrieval
BM25 ordnet Dokumente ein, indem es eine Relevanzbewertung auf der Grundlage der übereinstimmenden Suchbegriffe berechnet. Die Bewertung erfolgt auf Begriffsebene und wird auf Dokumentenebene aggregiert.
Bewertung auf Termebene
Für jeden Suchbegriff, der in einem Dokument vorkommt, berechnet BM25 eine Bewertung auf Termebene:
term_score =
IDF(term) ×
TF_boost(term, document, k1) ×
length_normalization(document, b)
Wobei:
IDF(term) gibt an, wie selten der Begriff in der Sammlung ist
TF_boost(..., k1) steigt mit der Termhäufigkeit, sättigt aber mit zunehmender Häufigkeit
length_normalization(..., b) passt die Punktzahl auf der Grundlage der Dokumentenlänge an
Scoring auf Dokumentenebene und Top-K Retrieval
Die endgültige Dokumentenbewertung ist die Summe der Bewertungen auf Termebene für alle übereinstimmenden Suchbegriffe:
document_score =
sum of term_score over all matched query terms
Die Dokumente werden nach ihren endgültigen Punktzahlen geordnet, und die Top-K-Dokumente mit den höchsten Punktzahlen werden zurückgegeben.
Bevor Sie beginnen
Bevor Sie die BM25-Funktion verwenden, planen Sie Ihr Sammlungsschema, um sicherzustellen, dass es die lexikalische Volltextsuche unterstützt:
Ein Textfeld für Rohinhalte
Ihre Sammlung muss ein
VARCHARFeld zur Speicherung von Rohtext enthalten. Dieses Feld ist die Quelle des Textes, der für die Volltextsuche verarbeitet werden soll.Ein Analysator für das Textfeld
Für das Textfeld muss ein Analyzer aktiviert sein. Der Analyzer definiert, wie der Text tokenisiert und normalisiert wird, bevor die lexikalische Relevanz von der BM25-Funktion berechnet wird.
Standardmäßig bietet Milvus einen eingebauten Analyzer, der Text auf der Grundlage von Leerzeichen und Interpunktion in Token umwandelt. Wenn Ihre Anwendung ein benutzerdefiniertes Tokenisierungs- oder Normalisierungsverhalten erfordert, können Sie einen benutzerdefinierten Analyzer definieren. Weitere Informationen finden Sie unter Wählen Sie den richtigen Analyzer für Ihren Anwendungsfall.
Ein spärlicher Vektor für die BM25-Ausgabe
Ihre Sammlung muss ein Feld
SPARSE_FLOAT_VECTORenthalten, um die von der BM25-Funktion erzeugten Sparse-Darstellungen zu speichern. Dieses Feld wird für die Indizierung und den Abruf bei der Volltextsuche verwendet.
Nachdem diese Überlegungen auf Schemaebene geklärt sind, fahren Sie mit der Erstellung der Sammlung und der Verwendung der BM25-Funktion fort.
Schritt 1: Erstellen einer Sammlung mit einer BM25-Funktion
Um die BM25-Funktion zu verwenden, müssen Sie sie bei der Erstellung der Sammlung definieren. Die Funktion wird Teil des Sammlungsschemas und wird automatisch beim Einfügen und Suchen von Daten angewendet.
Schemafelder definieren
Das Schema Ihrer Sammlung muss mindestens drei Pflichtfelder enthalten:
Primärfeld: Identifiziert jede Entität in der Sammlung eindeutig.
Textfeld (
VARCHAR): Speichert Rohtextdokumente. Sie müssenenable_analyzer=Trueeinstellen, damit Milvus den Text für die BM25-Relevanzeinstufung verarbeiten kann. Standardmäßig verwendet Milvus denstandardAnalysator für die Textanalyse. Um einen anderen Analyzer zu konfigurieren, siehe Analyzer-Übersicht.Sparse Vector Field (
SPARSE_FLOAT_VECTOR): Speichert spärliche Einbettungen, die automatisch von der BM25-Funktion generiert werden.
from pymilvus import MilvusClient, DataType, Function, FunctionType
client = MilvusClient(
uri="http://localhost:19530",
token="root:Milvus"
)
schema = client.create_schema()
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True) # Primary field
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1000, enable_analyzer=True) # Text field
schema.add_field(field_name="sparse", datatype=DataType.SPARSE_FLOAT_VECTOR) # Sparse vector field; no dim required for sparse vectors
import io.milvus.v2.common.DataType;
import io.milvus.v2.service.collection.request.AddFieldReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder()
.build();
schema.addField(AddFieldReq.builder()
.fieldName("id")
.dataType(DataType.Int64)
.isPrimaryKey(true)
.autoID(true)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("text")
.dataType(DataType.VarChar)
.maxLength(1000)
.enableAnalyzer(true)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("sparse")
.dataType(DataType.SparseFloatVector)
.build());
import (
"context"
"fmt"
"github.com/milvus-io/milvus/client/v2/column"
"github.com/milvus-io/milvus/client/v2/entity"
"github.com/milvus-io/milvus/client/v2/index"
"github.com/milvus-io/milvus/client/v2/milvusclient"
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
milvusAddr := "http://localhost:19530"
token := "root:Milvus"
client, err := milvusclient.New(ctx, &milvusclient.ClientConfig{
Address: milvusAddr,
APIKey: token
})
if err != nil {
fmt.Println(err.Error())
// handle error
}
defer client.Close(ctx)
schema := entity.NewSchema()
schema.WithField(entity.NewField().
WithName("id").
WithDataType(entity.FieldTypeInt64).
WithIsPrimaryKey(true).
WithIsAutoID(true),
).WithField(entity.NewField().
WithName("text").
WithDataType(entity.FieldTypeVarChar).
WithEnableAnalyzer(true).
WithMaxLength(1000),
).WithField(entity.NewField().
WithName("sparse").
WithDataType(entity.FieldTypeSparseVector),
)
import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";
const address = "http://localhost:19530";
const token = "root:Milvus";
const client = new MilvusClient({address, token});
const schema = [
{
name: "id",
data_type: DataType.Int64,
is_primary_key: true,
},
{
name: "text",
data_type: "VarChar",
enable_analyzer: true,
enable_match: true,
max_length: 1000,
},
{
name: "sparse",
data_type: DataType.SparseFloatVector,
},
];
console.log(res.results)
export schema='{
"autoId": true,
"enabledDynamicField": false,
"fields": [
{
"fieldName": "id",
"dataType": "Int64",
"isPrimary": true
},
{
"fieldName": "text",
"dataType": "VarChar",
"elementTypeParams": {
"max_length": 1000,
"enable_analyzer": true
}
},
{
"fieldName": "sparse",
"dataType": "SparseFloatVector"
}
]
}'
Definieren Sie die BM25-Funktion
Die BM25-Funktion wandelt tokenisierten Text in Sparse-Vektoren um, die die BM25-Bewertung unterstützen.
Definieren Sie die Funktion und fügen Sie sie zu Ihrem Schema hinzu:
bm25_function = Function(
name="text_bm25_emb", # Function name
input_field_names=["text"], # Name of the VARCHAR field containing raw text data
output_field_names=["sparse"], # Name of the SPARSE_FLOAT_VECTOR field reserved to store generated embeddings
function_type=FunctionType.BM25, # Set to `BM25`
)
schema.add_function(bm25_function)
import io.milvus.common.clientenum.FunctionType;
import io.milvus.v2.service.collection.request.CreateCollectionReq.Function;
import java.util.*;
schema.addFunction(Function.builder()
.functionType(FunctionType.BM25)
.name("text_bm25_emb")
.inputFieldNames(Collections.singletonList("text"))
.outputFieldNames(Collections.singletonList("sparse"))
.build());
function := entity.NewFunction().
WithName("text_bm25_emb").
WithInputFields("text").
WithOutputFields("sparse").
WithType(entity.FunctionTypeBM25)
schema.WithFunction(function)
const functions = [
{
name: 'text_bm25_emb',
description: 'bm25 function',
type: FunctionType.BM25,
input_field_names: ['text'],
output_field_names: ['sparse'],
params: {},
},
];
export schema='{
"autoId": true,
"enabledDynamicField": false,
"fields": [
{
"fieldName": "id",
"dataType": "Int64",
"isPrimary": true
},
{
"fieldName": "text",
"dataType": "VarChar",
"elementTypeParams": {
"max_length": 1000,
"enable_analyzer": true
}
},
{
"fieldName": "sparse",
"dataType": "SparseFloatVector"
}
],
"functions": [
{
"name": "text_bm25_emb",
"type": "BM25",
"inputFieldNames": ["text"],
"outputFieldNames": ["sparse"],
"params": {}
}
]
}'
Konfigurieren Sie den Index
Nachdem Sie das Schema mit den erforderlichen Feldern und der integrierten Funktion definiert haben, richten Sie den Index für Ihre Sammlung ein.
index_params = client.prepare_index_params()
index_params.add_index(
field_name="sparse",
index_type="SPARSE_INVERTED_INDEX",
metric_type="BM25",
params={
"inverted_index_algo": "DAAT_MAXSCORE",
"bm25_k1": 1.2,
"bm25_b": 0.75
}
)
import io.milvus.v2.common.IndexParam;
Map<String,Object> params = new HashMap<>();
params.put("inverted_index_algo", "DAAT_MAXSCORE");
params.put("bm25_k1", 1.2);
params.put("bm25_b", 0.75);
List<IndexParam> indexes = new ArrayList<>();
indexes.add(IndexParam.builder()
.fieldName("sparse")
.indexType(IndexParam.IndexType.AUTOINDEX)
.metricType(IndexParam.MetricType.BM25)
.extraParams(params)
.build());
indexOption := milvusclient.NewCreateIndexOption("my_collection", "sparse",
index.NewAutoIndex(entity.MetricType(entity.BM25)))
.WithExtraParam("inverted_index_algo", "DAAT_MAXSCORE")
.WithExtraParam("bm25_k1", 1.2)
.WithExtraParam("bm25_b", 0.75)
const index_params = [
{
field_name: "sparse",
metric_type: "BM25",
index_type: "SPARSE_INVERTED_INDEX",
params: {
"inverted_index_algo": "DAAT_MAXSCORE",
"bm25_k1": 1.2,
"bm25_b": 0.75
}
},
];
export indexParams='[
{
"fieldName": "sparse",
"metricType": "BM25",
"indexType": "AUTOINDEX",
"params":{
"inverted_index_algo": "DAAT_MAXSCORE",
"bm25_k1": 1.2,
"bm25_b": 0.75
}
}
]'
Erstellen Sie die Sammlung
Erstellen Sie nun die Sammlung unter Verwendung des Schemas und der definierten Indexparameter:
client.create_collection(
collection_name='my_collection',
schema=schema,
index_params=index_params
)
import io.milvus.v2.service.collection.request.CreateCollectionReq;
CreateCollectionReq requestCreate = CreateCollectionReq.builder()
.collectionName("my_collection")
.collectionSchema(schema)
.indexParams(indexes)
.build();
client.createCollection(requestCreate);
err = client.CreateCollection(ctx,
milvusclient.NewCreateCollectionOption("my_collection", schema).
WithIndexOptions(indexOption))
if err != nil {
fmt.Println(err.Error())
// handle error
}
await client.create_collection(
collection_name: 'my_collection',
schema: schema,
index_params: index_params,
functions: functions
);
export CLUSTER_ENDPOINT="http://localhost:19530"
export TOKEN="root:Milvus"
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/collections/create" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d "{
\"collectionName\": \"my_collection\",
\"schema\": $schema,
\"indexParams\": $indexParams
}"
Sobald die Sammlung mit einer BM25-Funktion erstellt ist, können Sie Text einfügen und lexikalische Suchen auf der Grundlage von Textabfragen durchführen.
Schritt 2: Einfügen von Textdaten in die Sammlung
Nachdem Sie Ihre Sammlung und Ihren Index eingerichtet haben, können Sie nun Textdaten einfügen. Bei diesem Vorgang müssen Sie nur den Rohtext bereitstellen. Die Funktion BM25, die wir zuvor definiert haben, erzeugt automatisch den Sparse-Vektor für jeden Texteintrag.
client.insert('my_collection', [
{'text': 'information retrieval is a field of study.'},
{'text': 'information retrieval focuses on finding relevant information in large datasets.'},
{'text': 'data mining and information retrieval overlap in research.'},
])
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.milvus.v2.service.vector.request.InsertReq;
Gson gson = new Gson();
List<JsonObject> rows = Arrays.asList(
gson.fromJson("{\"text\": \"information retrieval is a field of study.\"}", JsonObject.class),
gson.fromJson("{\"text\": \"information retrieval focuses on finding relevant information in large datasets.\"}", JsonObject.class),
gson.fromJson("{\"text\": \"data mining and information retrieval overlap in research.\"}", JsonObject.class)
);
client.insert(InsertReq.builder()
.collectionName("my_collection")
.data(rows)
.build());
// go
await client.insert({
collection_name: 'my_collection',
data: [
{'text': 'information retrieval is a field of study.'},
{'text': 'information retrieval focuses on finding relevant information in large datasets.'},
{'text': 'data mining and information retrieval overlap in research.'},
]);
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/insert" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d '{
"data": [
{"text": "information retrieval is a field of study."},
{"text": "information retrieval focuses on finding relevant information in large datasets."},
{"text": "data mining and information retrieval overlap in research."}
],
"collectionName": "my_collection"
}'
Schritt 3: Suche mit Textabfrage
Sobald Sie Daten in Ihre Sammlung eingefügt haben, können Sie Volltextsuchen mit Rohtextabfragen durchführen. Milvus wandelt Ihre Abfrage automatisch in einen Sparse-Vektor um und ordnet die übereinstimmenden Suchergebnisse mit Hilfe des BM25-Algorithmus ein und gibt dann die TopK (limit) Ergebnisse zurück.
search_params = {
}
res = client.search(
collection_name='my_collection',
data=['whats the focus of information retrieval?'],
anns_field='sparse',
output_fields=['text'], # Fields to return in search results; sparse field cannot be output
limit=3,
search_params=search_params
)
print(res)
import io.milvus.v2.service.vector.request.SearchReq;
import io.milvus.v2.service.vector.request.data.EmbeddedText;
import io.milvus.v2.service.vector.response.SearchResp;
Map<String,Object> searchParams = new HashMap<>();
SearchResp searchResp = client.search(SearchReq.builder()
.collectionName("my_collection")
.data(Collections.singletonList(new EmbeddedText("whats the focus of information retrieval?")))
.annsField("sparse")
.topK(3)
.searchParams(searchParams)
.outputFields(Collections.singletonList("text"))
.build());
annSearchParams := index.NewCustomAnnParam()
resultSets, err := client.Search(ctx, milvusclient.NewSearchOption(
"my_collection", // collectionName
3, // limit
[]entity.Vector{entity.Text("whats the focus of information retrieval?")},
).WithConsistencyLevel(entity.ClStrong).
WithANNSField("sparse").
WithAnnParam(annSearchParams).
WithOutputFields("text"))
if err != nil {
fmt.Println(err.Error())
// handle error
}
for _, resultSet := range resultSets {
fmt.Println("IDs: ", resultSet.IDs.FieldData().GetScalars())
fmt.Println("Scores: ", resultSet.Scores)
fmt.Println("text: ", resultSet.GetColumn("text").FieldData().GetScalars())
}
await client.search(
collection_name: 'my_collection',
data: ['whats the focus of information retrieval?'],
anns_field: 'sparse',
output_fields: ['text'],
limit: 3,
)
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/search" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
--data-raw '{
"collectionName": "my_collection",
"data": [
"whats the focus of information retrieval?"
],
"annsField": "sparse",
"limit": 3,
"outputFields": [
"text"
],
"searchParams":{
"params":{}
}
}'