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 표현은 확장 가능한 검색을 위해 용어 게시물이 노드 간에 분할되어 있는 스파스 임베딩에 삽입됩니다.
쿼리 텍스트 프로세스: 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)
Where:
IDF(term)은 컬렉션에서 용어가 얼마나 희귀한지를 반영합니다.
TF_boost(..., k1)는 용어 빈도에 따라 증가하지만 빈도가 증가하면 포화 상태가 됩니다.
length_normalization(..., b)는 문서 길이에 따라 점수를 조정합니다.
문서 수준 점수 및 Top-K 검색
최종 문서 점수는 일치하는 모든 쿼리 용어에 대한 용어 수준 점수의 합계입니다:
document_score =
sum of term_score over all matched query terms
문서는 최종 점수에 따라 순위가 매겨지며, 가장 높은 점수를 받은 상위 K개의 문서가 반환됩니다.
시작하기 전에
BM25 기능을 사용하기 전에 컬렉션 스키마를 계획하여 어휘 전체 텍스트 검색을 지원하는지 확인하세요:
원시 콘텐츠를 위한 텍스트 필드
컬렉션에는 원시 텍스트를 저장할
VARCHAR필드가 포함되어야 합니다. 이 필드는 전체 텍스트 검색을 위해 처리될 텍스트의 소스입니다.텍스트 필드용 분석기
텍스트 필드에는 분석기가 활성화되어 있어야 합니다. 분석기는 BM25 함수에 의해 어휘 관련성이 계산되기 전에 텍스트가 토큰화되고 정규화되는 방식을 정의합니다.
기본적으로 Milvus는 공백과 구두점을 기반으로 텍스트를 토큰화하는 기본 제공 분석기를 제공합니다. 애플리케이션에 사용자 지정 토큰화 또는 정규화 동작이 필요한 경우, 사용자 지정 분석기를 정의할 수 있습니다. 자세한 내용은 사용 사례에 적합한 분석기 선택하기를 참조하세요.
BM25 출력을 위한 스파스 벡터
컬렉션에는 BM25 함수에 의해 생성된 스파스 표현을 저장하기 위한
SPARSE_FLOAT_VECTOR필드가 포함되어야 합니다. 이 필드는 전체 텍스트 검색 중 색인 및 검색에 사용됩니다.
이러한 스키마 수준의 고려 사항을 파악한 후 컬렉션을 만들고 BM25 함수를 사용하세요.
1단계: BM25 함수를 사용하여 컬렉션 만들기
BM25 함수를 사용하려면 컬렉션을 만들 때 이를 정의해야 합니다. 이 함수는 컬렉션 스키마의 일부가 되어 데이터 삽입 및 검색 중에 자동으로 적용됩니다.
스키마 필드 정의
컬렉션 스키마에는 최소 3개의 필수 필드가 포함되어야 합니다:
기본 필드: 컬렉션의 각 엔티티를 고유하게 식별합니다.
텍스트 필드 (
VARCHAR): 원시 텍스트 문서를 저장합니다. Milvus가 BM25 관련성 순위를 위해 텍스트를 처리할 수 있도록enable_analyzer=True을 설정해야 합니다. 기본적으로 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 알고리즘을 사용하여 일치하는 검색 결과의 순위를 매긴 다음 상위 K (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":{}
}
}'