Fungsi BM25
Fungsi BM25 memungkinkan pencarian teks lengkap dengan mengubah teks mentah menjadi vektor yang jarang dan menilai dokumen berdasarkan relevansi leksikal. Fungsi ini menerapkan pencocokan berbasis istilah dan pembobotan yang sadar frekuensi untuk mendukung pengambilan dokumen teks yang efisien yang sangat cocok dengan istilah kueri.
Sebagai fungsi teks lokal, fungsi BM25 berjalan di dalam Milvus dan tidak memerlukan inferensi model atau integrasi eksternal. Fungsi ini menyediakan mekanisme pengambilan yang deterministik dan transparan untuk skenario pencarian berbasis teks.
Bagaimana BM25 bekerja
Algoritma BM25 adalah algoritma penilaian relevansi berbasis istilah yang banyak digunakan dalam pencarian teks lengkap. Di Milvus, BM25 diimplementasikan sebagai pipeline pencarian jarang yang mengubah teks menjadi representasi bobot istilah dan mengambil dokumen K teratas menggunakan indeks jarang terdistribusi.
Alur kerja secara keseluruhan terdiri dari dua jalur simetris: pemasukan dokumen dan pemrosesan teks kueri, yang memiliki logika analisis teks yang sama.
Pemasukan dokumen: Dari teks ke representasi jarang
Ketika sebuah dokumen dimasukkan, teks mentahnya pertama-tama diproses oleh penganalisis, yang menandai teks ke dalam istilah-istilah individual.
Misalnya, dokumen:
"We are loving Milvus!"
dapat dianalisis menjadi beberapa istilah berikut:
["we", "love", "milvus"]
Setiap dokumen kemudian direpresentasikan sebagai representasi frekuensi istilah (TF), yang mencatat berapa kali setiap istilah muncul dalam dokumen. Sebagai contoh:
{
"we": 1,
"love": 1,
"milvus": 1
}
Pada saat yang sama, Milvus memperbarui statistik tingkat korpus, termasuk:
frekuensi dokumen (DF) dari setiap istilah
panjang dokumen rata-rata
memposting daftar yang memetakan setiap istilah ke dokumen yang mengandungnya
Representasi TF dokumen dimasukkan ke dalam sematan jarang, di mana postingan term dipartisi di seluruh node untuk pengambilan yang terukur.
Proses teks kueri: Menerapkan pembobotan IDF
Ketika kueri berbasis teks dikeluarkan, kueri tersebut diproses oleh penganalisis yang sama dengan yang digunakan selama pemasukan dokumen, untuk memastikan segmentasi term yang konsisten.
Misalnya, kueri:
"who loves Milvus?"
dapat dianalisis menjadi:
["who", "love", "milvus"]
Untuk setiap istilah kueri, Milvus mencari inverse document frequency (IDF) dari statistik korpus. IDF mencerminkan seberapa informatif sebuah istilah di seluruh kumpulan data: istilah yang lebih jarang mendapat bobot yang lebih tinggi, sementara istilah yang umum mendapat bobot yang lebih rendah.
Secara konseptual, hal ini menghasilkan sekumpulan istilah kueri berbobot IDF, seperti:
{
"who": 0.1,
"love": 0.5,
"milvus": 1.2
}
Penilaian BM25 dan pengambilan K teratas
BM25 memberi peringkat dokumen dengan menghitung skor relevansi berdasarkan istilah kueri yang cocok. Penilaian dilakukan pada tingkat istilah dan digabungkan pada tingkat dokumen.
Penilaian tingkat istilah
Untuk setiap istilah kueri yang muncul dalam dokumen, BM25 menghitung skor tingkat istilah:
term_score =
IDF(term) ×
TF_boost(term, document, k1) ×
length_normalization(document, b)
Di mana:
IDF (term) mencerminkan seberapa langka istilah tersebut dalam koleksi
TF_boost(..., k1) meningkat seiring dengan frekuensi term, tetapi akan menjadi jenuh seiring dengan bertambahnya frekuensi
length_normalization(..., b) menyesuaikan skor berdasarkan panjang dokumen
Penilaian tingkat dokumen dan pengambilan Top-K
Skor dokumen akhir adalah jumlah skor tingkat istilah untuk semua istilah kueri yang cocok:
document_score =
sum of term_score over all matched query terms
Dokumen diurutkan berdasarkan skor akhirnya, dan dokumen dengan skor tertinggi Top-K dikembalikan.
Sebelum Anda mulai
Sebelum menggunakan fungsi BM25, rencanakan skema koleksi Anda untuk memastikan skema tersebut mendukung pencarian teks lengkap leksikal:
Bidang teks untuk konten mentah
Koleksi Anda harus menyertakan bidang
VARCHARuntuk menyimpan teks mentah. Bidang ini merupakan sumber teks yang akan diproses untuk pencarian teks lengkap.Penganalisis untuk bidang teks
Bidang teks harus memiliki penganalisis yang diaktifkan. Penganalisis mendefinisikan bagaimana teks ditandai dan dinormalisasi sebelum relevansi leksikal dihitung oleh fungsi BM25.
Secara default, Milvus menyediakan penganalisis bawaan yang menokenisasi teks berdasarkan spasi dan tanda baca. Jika aplikasi Anda memerlukan perilaku tokenisasi atau normalisasi khusus, Anda dapat menentukan penganalisis khusus. Lihat Memilih Penganalisis yang Tepat untuk Kasus Penggunaan Anda untuk detailnya.
Vektor yang jarang untuk keluaran BM25
Koleksi Anda harus menyertakan bidang
SPARSE_FLOAT_VECTORuntuk menyimpan representasi jarang yang dihasilkan oleh fungsi BM25. Bidang ini digunakan untuk pengindeksan dan pengambilan selama pencarian teks lengkap.
Setelah pertimbangan tingkat skema ini diketahui, lanjutkan dengan membuat koleksi dan menggunakan fungsi BM25.
Langkah 1: Membuat koleksi dengan fungsi BM25
Untuk menggunakan fungsi BM25, Anda harus mendefinisikannya saat membuat koleksi. Fungsi ini menjadi bagian dari skema koleksi dan diterapkan secara otomatis selama penyisipan dan pencarian data.
Menentukan bidang skema
Skema koleksi Anda harus menyertakan setidaknya tiga bidang wajib:
Bidang utama: Mengidentifikasi secara unik setiap entitas dalam koleksi.
Bidang teks (
VARCHAR): Menyimpan dokumen teks mentah. Harus menetapkanenable_analyzer=Trueagar Milvus dapat memproses teks untuk peringkat relevansi BM25. Secara default, Milvus menggunakan fiturstandardanalyzer untuk analisis teks. Untuk mengonfigurasi penganalisis yang berbeda, lihat Ikhtisar Penganalisis.Bidang vektor jarang (
SPARSE_FLOAT_VECTOR): Menyimpan sematan jarang yang dihasilkan secara otomatis oleh fungsi 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"
}
]
}'
Mendefinisikan fungsi BM25
Fungsi BM25 mengubah teks yang diberi token menjadi vektor jarang yang mendukung penilaian BM25.
Tentukan fungsi dan tambahkan ke skema Anda:
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": {}
}
]
}'
Mengonfigurasi indeks
Setelah mendefinisikan skema dengan bidang yang diperlukan dan fungsi bawaan, siapkan indeks untuk koleksi Anda.
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
}
}
]'
Membuat koleksi
Sekarang buat koleksi menggunakan skema dan parameter indeks yang telah ditentukan:
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
}"
Setelah koleksi dengan fungsi BM25 dibuat, Anda dapat menyisipkan teks dan melakukan pencarian leksikal berdasarkan kueri teks.
Langkah 2: Masukkan data teks ke dalam koleksi
Setelah menyiapkan koleksi dan indeks Anda, Anda siap memasukkan data teks. Dalam proses ini, Anda hanya perlu menyediakan teks mentah. Fungsi BM25 yang telah kita definisikan sebelumnya secara otomatis menghasilkan vektor jarang untuk setiap entri teks.
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"
}'
Langkah 3: Cari dengan kueri teks
Setelah Anda memasukkan data ke dalam koleksi Anda, Anda dapat melakukan pencarian teks lengkap menggunakan kueri teks mentah. Milvus secara otomatis mengubah kueri Anda menjadi vektor jarang dan mengurutkan hasil pencarian yang cocok menggunakan algoritme BM25, dan kemudian mengembalikan hasil 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":{}
}
}'