Fonction BM25
La fonction BM25 permet la recherche en texte intégral en transformant le texte brut en vecteurs épars et en notant les documents en fonction de leur pertinence lexicale. Elle applique une correspondance basée sur les termes et une pondération tenant compte de la fréquence afin de permettre une récupération efficace des documents textuels qui correspondent étroitement aux termes de la requête.
En tant que fonction textuelle locale, la fonction BM25 s'exécute dans Milvus et ne nécessite pas d'inférence de modèle ou d'intégrations externes. Elle fournit un mécanisme d'extraction déterministe et transparent pour les scénarios de recherche textuelle.
Fonctionnement de la fonction BM25
L'algorithme BM25 est un algorithme de notation de la pertinence basé sur les termes, largement utilisé dans la recherche de texte intégral. Dans Milvus, BM25 est mis en œuvre sous la forme d'un pipeline d'extraction clairsemé qui convertit le texte en représentations de poids des termes et extrait les K premiers documents à l'aide d'index clairsemés distribués.
Le flux de travail global se compose de deux voies symétriques : l'ingestion de documents et le traitement des requêtes, qui partagent la même logique d'analyse de texte.
L'ingestion de documents : Du texte à la représentation éparse
Lorsqu'un document est inséré, son texte brut est d'abord traité par un analyseur, qui le transforme en termes individuels.
Par exemple, le document :
"We are loving Milvus!"
peut être analysé dans les termes suivants :
["we", "love", "milvus"]
Chaque document est ensuite représenté sous la forme d'une représentation de la fréquence des termes (TF), qui enregistre le nombre d'occurrences de chaque terme dans le document. Par exemple :
{
"we": 1,
"love": 1,
"milvus": 1
}
En même temps, Milvus met à jour les statistiques au niveau du corpus, y compris :
la fréquence des documents (FD) de chaque terme
la longueur moyenne des documents
les listes d'affichage qui associent chaque terme aux documents qui le contiennent.
La représentation de la TF du document est insérée dans des encastrements clairsemés, où les affichages de termes sont répartis entre les nœuds pour une recherche évolutive.
Traitement du texte de la requête : Appliquer la pondération IDF
Lorsqu'une requête textuelle est émise, elle est traitée par le même analyseur que celui utilisé lors de l'ingestion des documents, ce qui garantit une segmentation cohérente des termes.
Par exemple, la requête :
"who loves Milvus?"
peut être analysée en :
["who", "love", "milvus"]
Pour chaque terme de la requête, Milvus recherche sa fréquence inverse de document (IDF) à partir des statistiques du corpus. L'IDF reflète le degré d'information d'un terme dans l'ensemble de la base de données : les termes plus rares reçoivent une pondération plus élevée, tandis que les termes courants reçoivent une pondération plus faible.
Conceptuellement, cela produit un ensemble de termes d'interrogation pondérés par l'IDF, comme par exemple :
{
"who": 0.1,
"love": 0.5,
"milvus": 1.2
}
Notation BM25 et recherche top K
BM25 classe les documents en calculant un score de pertinence basé sur la correspondance des termes de la requête. La notation est effectuée au niveau des termes et agrégée au niveau des documents.
Notation au niveau des termes
Pour chaque terme de la requête apparaissant dans un document, BM25 calcule un score au niveau du terme :
term_score =
IDF(term) ×
TF_boost(term, document, k1) ×
length_normalization(document, b)
Où :
IDF(terme) reflète la rareté du terme dans la collection
TF_boost(..., k1) augmente avec la fréquence du terme mais sature lorsque la fréquence augmente
length_normalization(..., b) ajuste le score en fonction de la longueur du document.
Notation au niveau du document et recherche Top-K
Le score final du document est la somme des scores au niveau des termes pour tous les termes de la requête correspondants :
document_score =
sum of term_score over all matched query terms
Les documents sont classés en fonction de leur score final, et les K documents ayant obtenu les meilleurs scores sont retournés.
Avant de commencer
Avant d'utiliser la fonction BM25, planifiez le schéma de votre collection pour vous assurer qu'il prend en charge la recherche lexicale en texte intégral :
Un champ de texte pour le contenu brut
Votre collection doit comporter un champ
VARCHARpour stocker le texte brut. Ce champ est la source du texte qui sera traité pour la recherche plein texte.Un analyseur pour le champ de texte
Un analyseur doit être activé dans le champ de texte. L'analyseur définit la manière dont le texte est tokenisé et normalisé avant que la pertinence lexicale ne soit calculée par la fonction BM25.
Par défaut, Milvus fournit un analyseur intégré qui symbolise le texte en fonction des espaces blancs et de la ponctuation. Si votre application nécessite un comportement de tokénisation ou de normalisation personnalisé, vous pouvez définir un analyseur personnalisé. Voir Choisir le bon analyseur pour votre cas d'utilisation pour plus de détails.
Un vecteur clairsemé pour la sortie BM25
Votre collection doit inclure un champ
SPARSE_FLOAT_VECTORpour stocker les représentations éparses générées par la fonction BM25. Ce champ est utilisé pour l'indexation et l'extraction lors d'une recherche en texte intégral.
Une fois que ces considérations au niveau du schéma sont réglées, vous pouvez créer la collection et utiliser la fonction BM25.
Étape 1 : Créer une collection avec une fonction BM25
Pour utiliser la fonction BM25, vous devez la définir lors de la création de la collection. La fonction fait partie du schéma de la collection et est appliquée automatiquement lors de l'insertion et de la recherche de données.
Définir les champs du schéma
Le schéma de votre collection doit comprendre au moins trois champs obligatoires :
Champ primaire: Il identifie de manière unique chaque entité de la collection.
Champ texte (
VARCHAR) : Stocke les documents textuels bruts. Vous devez définirenable_analyzer=Trueafin que Milvus puisse traiter le texte pour le classement par pertinence de BM25. Par défaut, Milvus utilise l'analyseurstandardpour l' analyse du texte. Pour configurer un autre analyseur, reportez-vous à la section Présentation de l'analyseur.Champ vectoriel clair (
SPARSE_FLOAT_VECTOR) : Stocke les encastrements épars générés automatiquement par la fonction BM25.
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"
}
]
}'
Définition de la fonction BM25
La fonction BM25 convertit le texte tokenisé en vecteurs épars qui prennent en charge l'évaluation BM25.
Définissez la fonction et ajoutez-la à votre schéma :
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": {}
}
]
}'
Configurer l'index
Après avoir défini le schéma avec les champs nécessaires et la fonction intégrée, configurez l'index pour votre collection.
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
}
}
]'
Créer la collection
Créez maintenant la collection en utilisant le schéma et les paramètres d'index définis :
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
}"
Une fois la collection créée avec une fonction BM25, vous pouvez insérer du texte et effectuer des recherches lexicales sur la base d'une requête textuelle.
Étape 2 : Insérer des données textuelles dans la collection
Après avoir configuré votre collection et votre index, vous êtes prêt à insérer des données textuelles. Pour ce faire, il vous suffit de fournir le texte brut. La fonction BM25 que nous avons définie précédemment génère automatiquement le vecteur épars pour chaque entrée de texte.
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"
}'
Étape 3 : Recherche à l'aide d'une requête textuelle
Une fois que vous avez inséré des données dans votre collection, vous pouvez effectuer des recherches en texte intégral à l'aide de requêtes de texte brut. Milvus convertit automatiquement votre requête en un vecteur clair et classe les résultats de recherche correspondants à l'aide de l'algorithme BM25, puis renvoie les topK (limit) résultats.
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":{}
}
}'