BM25 功能
BM25 函式可將原始文字轉換為稀疏向量,並根據詞彙相關性對文件進行評分,從而實現全文本搜尋。它應用了基於詞彙的匹配和頻率感知權重,以支援與查詢詞彙密切匹配的文本文檔的高效檢索。
作為一個本機文字函式,BM25 函式在 Milvus 內執行,不需要模型推論或外部整合。它為基於文字的搜尋情況提供了確定且透明的檢索機制。
BM25 如何運作
BM25演算法是一種基於詞彙的相關性評分演算法,廣泛應用於全文檢索。在 Milvus 中,BM25 實作為稀疏檢索管道,可將文字轉換為詞彙權重表示,並使用分散式稀疏索引檢索前K 個文件。
整體工作流程包含兩個對稱的路徑:文件擷取和查詢文字處理,它們共用相同的文字分析邏輯。
文件擷取:從文字到稀疏表示
插入文件時,其原始文字會先由分析器處理,分析器會將文字標記化為個別術語。
例如,文件:
"We are loving Milvus!"
可分析為下列詞彙:
["we", "love", "milvus"]
然後,每個文件會以詞彙頻率 (TF) 表示,記錄每個詞彙在文件中出現的次數。舉例來說
{
"we": 1,
"love": 1,
"milvus": 1
}
同時,Milvus 會更新語料庫層級的統計資料,包括
每個詞彙的文檔頻率 (DF)
平均文件長度
將每個詞彙映射到包含該詞彙的文檔的發佈清單
將文件的 TF 表示法插入稀疏嵌入(sparse embeddings),其中的詞彙發佈會在節點間進行分割,以便進行可擴展性檢索。
查詢文字處理:套用 IDF 加權
當發出基於文字的查詢時,會由在文件擷取過程中使用的相同分析器來處理,以確保術語分割的一致性。
例如,查詢
"who loves Milvus?"
可以被分析成
["who", "love", "milvus"]
對於每個查詢詞彙,Milvus 會從語料庫統計中查詢其反向文檔頻率(IDF)。IDF 反映了一個詞彙在整個資料集中的資訊量:較罕見的詞彙會得到較高的權重,而常見的詞彙會得到較低的權重。
在概念上,這會產生一組 IDF 加權的查詢詞,例如:
{
"who": 0.1,
"love": 0.5,
"milvus": 1.2
}
BM25 評分與前 K 檢索
BM25 根據匹配的查詢字詞計算相關性分數,從而對文件進行排序。計分在詞彙層級執行,並在文件層級彙總。
詞彙層級計分
對於出現在文件中的每個查詢詞彙,BM25 會計算詞彙層級的得分:
term_score =
IDF(term) ×
TF_boost(term, document, k1) ×
length_normalization(document, b)
其中:
IDF(term)反映該詞彙在集合中的罕見程度
TF_boost(...,k1)隨著詞彙頻率的增加而增加,但隨著頻率的增加而飽和
length_normalization(..., b)根據文件長度調整得分
文件層級計分和 Top-K 檢索
最終的文件分數是所有匹配查詢字詞的字詞層級分數的總和:
document_score =
sum of term_score over all matched query terms
文檔會依最終得分排序,並傳送得分最高的 Top-K 文檔。
開始之前
使用 BM25 功能之前,請規劃您的資料庫模式,以確保它支援詞彙全文檢索:
原始內容的文字欄位
您的資料集中必須包含一個
VARCHAR欄位來儲存原始文字。這個欄位是將被處理以進行全文檢索的文字來源。文字欄位的分析器
文字欄位必須啟用分析器。分析器定義了在 BM25 函式計算詞彙相關性之前,如何將文字標記化和規範化。
預設情況下,Milvus 提供內建分析器,根據空白和標點符號化文字。如果您的應用程式需要自訂標記化或規範化行為,您可以定義一個自訂分析器。詳情請參閱為您的使用個案選擇正確的分析器。
BM25 輸出的稀疏向量
您的集合必須包含
SPARSE_FLOAT_VECTOR欄位,以儲存 BM25 函式所產生的稀疏表示。這個欄位用於全文檢索時的索引和檢索。
想好這些模式層級的注意事項後,請繼續建立資料集並使用 BM25 函式。
步驟 1:使用 BM25 函式建立資料庫
若要使用 BM25 函式,您必須在建立資料集時定義它。該函數會成為集合模式的一部分,並在資料插入和搜尋時自動套用。
定義模式欄位
您的集合模式必須包含至少三個必填欄位:
主要欄位:唯一識別資料集中的每個實體。
文字欄位(
VARCHAR):儲存原始文字文件。必須設定enable_analyzer=True,以便 Milvus 能夠為 BM25 相關性排序處理文字。預設情況下,Milvus 使用standard分析器進行文字分析。若要設定不同的分析器,請參閱分析器總覽。稀疏向量場(
SPARSE_FLOAT_VECTOR):儲存由 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"
}
]
}'
定義 BM25 函式
BM25 函式會將標記化的文字轉換成支援 BM25 計分的稀疏向量。
定義函數並將其加入您的模式:
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": {}
}
]
}'
設定索引
定義包含必要欄位和內建函式的模式後,為您的集合設定索引。
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
}
}
]'
建立集合
現在使用已定義的模式和索引參數建立集合:
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
}"
一旦建立了具有 BM25 功能的集合,您就可以插入文字,並根據文字查詢執行詞彙搜尋。
步驟 2:將文字資料插入資料集中
設定好資料集和索引後,您就可以插入文字資料了。在這個過程中,您只需要提供原始文字。我們之前定義的 BM25 函式會自動為每個文字項目產生稀疏向量。
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"
}'
步驟 3:使用文字查詢進行搜尋
將資料插入資料庫後,您就可以使用原始文字查詢來執行全文檢索。Milvus 會自動將您的查詢轉換成稀疏向量,並使用 BM25 演算法對匹配的搜尋結果進行排序,然後傳回 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":{}
}
}'