Funzione BM25
La funzione BM25 consente la ricerca full text trasformando il testo grezzo in vettori sparsi e assegnando un punteggio ai documenti in base alla rilevanza lessicale. Applica una corrispondenza basata sui termini e una ponderazione consapevole della frequenza per supportare un recupero efficiente dei documenti di testo che corrispondono strettamente ai termini della query.
Come funzione testuale locale, la funzione BM25 viene eseguita all'interno di Milvus e non richiede l'inferenza del modello o integrazioni esterne. Fornisce un meccanismo di recupero deterministico e trasparente per gli scenari di ricerca basati sul testo.
Come funziona BM25
L'algoritmo BM25 è un algoritmo di punteggio di rilevanza basato sui termini, ampiamente utilizzato nel reperimento di testi completi. In Milvus, BM25 è implementato come una pipeline di reperimento rada che converte il testo in rappresentazioni di peso dei termini e recupera i documenti top K utilizzando indici radi distribuiti.
Il flusso di lavoro complessivo consiste in due percorsi simmetrici: l'ingestione dei documenti e l'elaborazione delle query, che condividono la stessa logica di analisi del testo.
Ingestione dei documenti: Dal testo alla rappresentazione rada
Quando si inserisce un documento, il suo testo grezzo viene prima elaborato da un analizzatore, che lo tokenizza in termini individuali.
Ad esempio, il documento:
"We are loving Milvus!"
può essere analizzato nei seguenti termini:
["we", "love", "milvus"]
Ogni documento viene quindi rappresentato come una rappresentazione di frequenza dei termini (TF), che registra quante volte ogni termine compare nel documento. Ad esempio:
{
"we": 1,
"love": 1,
"milvus": 1
}
Allo stesso tempo, Milvus aggiorna le statistiche a livello di corpus, tra cui:
la frequenza dei documenti (DF) di ogni termine
la lunghezza media del documento
gli elenchi di posting che mappano ogni termine ai documenti che lo contengono.
La rappresentazione TF del documento è inserita in embedding sparsi, dove i termini postati sono partizionati tra i nodi per un recupero scalabile.
Processo di interrogazione del testo: Applicazione della ponderazione IDF
Quando viene emessa una query basata sul testo, viene elaborata dallo stesso analizzatore utilizzato durante l'ingestione dei documenti, garantendo una segmentazione coerente dei termini.
Ad esempio, la query:
"who loves Milvus?"
può essere analizzata in:
["who", "love", "milvus"]
Per ogni termine della query, Milvus cerca la sua frequenza documentale inversa (IDF) dalle statistiche del corpus. L'IDF riflette il grado di informazione di un termine nell'intero set di dati: i termini più rari ricevono un peso maggiore, mentre i termini comuni ricevono un peso minore.
Concettualmente, questo produce un insieme di termini di query ponderati con IDF, come ad esempio:
{
"who": 0.1,
"love": 0.5,
"milvus": 1.2
}
Punteggio BM25 e top K retrieval
BM25 classifica i documenti calcolando un punteggio di rilevanza basato sulla corrispondenza dei termini della query. Il punteggio viene eseguito a livello di termine e aggregato a livello di documento.
Punteggio a livello di termine
Per ogni termine della query che compare in un documento, BM25 calcola un punteggio a livello di termine:
term_score =
IDF(term) ×
TF_boost(term, document, k1) ×
length_normalization(document, b)
Dove:
IDF(termine) riflette la rarità del termine nella raccolta.
TF_boost(..., k1) aumenta con la frequenza del termine, ma si satura con l'aumento della frequenza
length_normalization(..., b) regola il punteggio in base alla lunghezza del documento.
Punteggio a livello di documento e recupero Top-K
Il punteggio finale del documento è la somma dei punteggi a livello di termine per tutti i termini della query:
document_score =
sum of term_score over all matched query terms
I documenti vengono classificati in base ai loro punteggi finali e vengono restituiti i primi K documenti con il punteggio più alto.
Prima di iniziare
Prima di utilizzare la funzione BM25, pianificare lo schema della raccolta per assicurarsi che supporti la ricerca lessicale full text:
Un campo di testo per il contenuto grezzo
La raccolta deve includere un campo
VARCHARper memorizzare il testo grezzo. Questo campo è la fonte del testo che verrà elaborato per la ricerca full text.Un analizzatore per il campo di testo
Il campo di testo deve avere un analizzatore abilitato. L'analizzatore definisce come il testo viene tokenizzato e normalizzato prima che la rilevanza lessicale venga calcolata dalla funzione BM25.
Per impostazione predefinita, Milvus fornisce un analizzatore integrato che tokenizza il testo in base agli spazi bianchi e alla punteggiatura. Se l'applicazione richiede un comportamento personalizzato di tokenizzazione o normalizzazione, è possibile definire un analizzatore personalizzato. Per maggiori dettagli, vedere Scegliere l'analizzatore giusto per il proprio caso d'uso.
Un vettore rado per l'output BM25
La collezione deve includere un campo
SPARSE_FLOAT_VECTORper memorizzare le rappresentazioni rade generate dalla funzione BM25. Questo campo viene usato per l'indicizzazione e il recupero durante la ricerca full text.
Dopo aver effettuato queste considerazioni a livello di schema, si può procedere alla creazione della collezione e all'utilizzo della funzione BM25.
Passo 1: creare una raccolta con una funzione BM25
Per utilizzare la funzione BM25, è necessario definirla durante la creazione della raccolta. La funzione diventa parte dello schema della collezione e viene applicata automaticamente durante l'inserimento e la ricerca dei dati.
Definire i campi dello schema
Lo schema della raccolta deve includere almeno tre campi obbligatori:
Campo primario: Identifica in modo univoco ogni entità della raccolta.
Campo di testo (
VARCHAR): Memorizza documenti di testo grezzo. Deve essere impostatoenable_analyzer=Truein modo che Milvus possa elaborare il testo per la classificazione di rilevanza BM25. Per impostazione predefinita, Milvus utilizza l'analizzatorestandardper l' analisi del testo. Per configurare un analizzatore diverso, fare riferimento a Panoramica dell'analizzatore.Campo vettoriale sparso (
SPARSE_FLOAT_VECTOR): Memorizza le incorporazioni rade generate automaticamente dalla funzione 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"
}
]
}'
Definire la funzione BM25
La funzione BM25 converte il testo tokenizzato in vettori sparsi che supportano il punteggio BM25.
Definire la funzione e aggiungerla allo schema:
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": {}
}
]
}'
Configurare l'indice
Dopo aver definito lo schema con i campi necessari e la funzione integrata, configurare l'indice per la collezione.
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
}
}
]'
Creare la collezione
Creare ora la collezione utilizzando lo schema e i parametri dell'indice definiti:
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
}"
Una volta creata la collezione con la funzione BM25, è possibile inserire il testo ed eseguire ricerche lessicali basate sull'interrogazione del testo.
Passo 2: Inserire i dati di testo nella collezione
Dopo aver impostato la collezione e l'indice, si è pronti a inserire i dati di testo. In questo processo, è sufficiente fornire il testo grezzo. La funzione BM25, definita in precedenza, genera automaticamente il vettore sparse per ogni voce di testo.
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"
}'
Fase 3: Ricerca con query di testo
Una volta inseriti i dati nella collezione, è possibile eseguire ricerche full text utilizzando query di testo grezzo. Milvus converte automaticamente la query in un vettore rado e classifica i risultati della ricerca utilizzando l'algoritmo BM25, per poi restituire i risultati topK (limit).
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":{}
}
}'