• О Милвусе
  • Начать
  • Концепции
  • Руководство пользователя
    • Коллекции
    • Схема и поля данных
    • Вставка и удаление
    • Индексы
    • Поиск
    • Вывод функций и моделей
    • Оптимизация хранения
    • Снимки
  • Импорт данных
  • Инструменты искусственного интеллекта
  • Руководство по администрированию
  • Инструменты
  • Интеграции
  • Учебники
  • Вопросы и ответы
  • API Reference

Функция BM25

Функция BM25 обеспечивает полнотекстовый поиск, преобразуя исходный текст в разреженные векторы и оценивая документы по лексической релевантности. Она применяет сопоставление на основе терминов и взвешивание с учетом частоты, чтобы поддерживать эффективный поиск текстовых документов, которые близко соответствуют терминам запроса.

Как локальная текстовая функция, функция BM25 работает внутри Milvus и не требует вывода модели или внешних интеграций. Она обеспечивает детерминированный и прозрачный механизм поиска в сценариях поиска по тексту.

Принцип работы BM25

Алгоритм BM25 - это алгоритм оценки релевантности на основе терминов, широко используемый в полнотекстовом поиске. В Milvus BM25 реализован как конвейер поиска с разреженными индексами, который преобразует текст в представления с весом термина и извлекает K лучших документов с помощью распределенных разреженных индексов.

Общий рабочий процесс состоит из двух симметричных этапов: приема документов и обработки текста запроса, которые используют одну и ту же логику анализа текста.

Ввод документов: От текста к разреженному представлению

При вставке документа его необработанный текст сначала обрабатывается анализатором, который разбивает текст на отдельные термины.

Например, документ:

"We are loving Milvus!"

может быть проанализирован на следующие термины:

["we", "love", "milvus"]

Затем каждый документ представляется в виде частотного представления терминов (TF), которое фиксирует, сколько раз каждый термин встречается в документе. Например:

{
  "we": 1,
  "love": 1,
  "milvus": 1
}

В то же время Milvus обновляет статистику на уровне корпуса, включая:

  • частоту документа (ЧД) каждого термина

  • средняя длина документа

  • списки постингов, которые сопоставляют каждый термин с содержащими его документами.

ЦФ-представление документа вставляется в разреженные вкрапления, где постинги терминов разбиваются по узлам для масштабируемого поиска.

Обработка текста запроса: Применение взвешивания 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(термин) отражает степень редкости термина в коллекции.

  • 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

    Ваша коллекция должна включать поле 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":{}
    }
}'