Полнотекстовый поиск

Полнотекстовый поиск — это функция, которая позволяет находить документы, содержащие определенные термины или фразы в текстовых наборах данных, а затем ранжировать результаты по релевантности. Эта функция преодолевает ограничения семантического поиска, который может упускать точные термины, гарантируя получение наиболее точных и контекстуально релевантных результатов. Кроме того, она упрощает векторный поиск, принимая входные данные в виде необработанного текста и автоматически преобразуя ваши текстовые данные в разреженные вложения без необходимости вручную генерировать векторные вложения.

Благодаря использованию алгоритма BM25 для оценки релевантности эта функция особенно ценна в сценариях генерации с расширением поиска (RAG), где она отдаёт приоритет документам, наиболее точно соответствующим конкретным поисковым терминам.

Благодаря интеграции полнотекстового поиска с семантическим плотным векторным поиском вы можете повысить точность и релевантность результатов поиска. Дополнительную информацию см. в разделе «Гибридный поиск».

Реализация BM25

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

Полнотекстовый поиск в Milvus осуществляется в соответствии со следующим рабочим процессом:

  1. Ввод исходного текста: вы вставляете текстовые документы или вводите запрос в виде простого текста; модели вложения не требуются.

  2. Анализ текста: Milvus использует анализатор для преобразования текста в значимые термины, которые можно индексировать и по которым можно выполнять поиск.

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

  4. Хранение в коллекции: Milvus сохраняет полученные разреженные вложения в коллекции для быстрого извлечения и ранжирования.

  5. Оценка релевантности по BM25: во время поиска Milvus применяет функцию оценки BM25 для расчёта релевантности документов и возвращает ранжированные результаты, наиболее соответствующие терминам запроса.

Full Text Search Полнотекстовый поиск

Чтобы использовать полнотекстовый поиск, выполните следующие основные шаги:

  1. Создание коллекции: настройте необходимые поля и определите функцию BM25, которая преобразует исходный текст в разреженные вложения.

  2. Вставьте данные: загрузите исходные текстовые документы в коллекцию.

  3. Выполните поиск: используйте запрос на естественном языке для получения результатов, отсортированных по релевантности BM25.

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

Определение полей схемы

Схема вашей коллекции должна включать как минимум три обязательных поля:

  • Основное поле: однозначно идентифицирует каждую сущность в коллекции.

  • Строковое поле (VARCHAR или TEXT): хранит исходные текстовые документы. Необходимо установить параметр enable_analyzer=True, чтобы Milvus мог обрабатывать текст для ранжирования по релевантности с помощью BM25. По умолчанию Milvus использует standard анализатор для анализа текста. Чтобы настроить другой анализатор, обратитесь к разделу «Обзор анализаторов». В примерах на этой странице используется VARCHAR; для длинного текста можно определить поле ввода как TEXT и опустить max_length. Полный пример см. в разделе «Текстовое поле».

  • Поле разреженных векторов (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 := "localhost:19530"
client, err := milvusclient.New(ctx, &milvusclient.ClientConfig{
    Address: milvusAddr,
})
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"
            }
        ]
    }'

В приведённой выше конфигурации

  • id: выступает в качестве первичного ключа и автоматически генерируется с помощью auto_id=True.

  • text: хранит исходные текстовые данные для операций полнотекстового поиска. Для этого поля можно использовать VARCHAR для текстов ограниченного объёма или TEXT для длинного исходного контента.

  • sparse: векторное поле, зарезервированное для хранения внутренне сгенерированных разреженных вложений для операций полнотекстового поиска. Тип данных должен быть SPARSE_FLOAT_VECTOR.

Определение функции BM25

Функция BM25 преобразует токенизированный текст в разреженные векторы, поддерживающие подсчёт оценки по алгоритму BM25.

Определите функцию и добавьте её в свою схему:

bm25_function = Function(
    name="text_bm25_emb", # Function name
    input_field_names=["text"], # Name of the VARCHAR or TEXT 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": {}
            }
        ]
    }'

Параметр

Описание

name

Имя функции. Эта функция преобразует исходный текст из поля « text » в разреженные векторы, совместимые с BM25, которые будут храниться в поле « sparse ».

input_field_names

Имя поля VARCHAR или TEXT, требующего преобразования текста в разреженный вектор. Для FunctionType.BM25 этот параметр принимает только одно имя поля.

output_field_names

Имя поля, в котором будут храниться внутренне сгенерированные разреженные векторы. Для параметра FunctionType.BM25 этот параметр принимает только одно имя поля.

function_type

Тип используемой функции. Должен быть FunctionType.BM25.

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

Настройка индекса

После определения схемы с необходимыми полями и встроенной функцией настройте индекс для вашей коллекции.

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
            }
        }
    ]'

Параметр

Описание

field_name

Имя векторного поля, которое нужно проиндексировать. Для полнотекстового поиска это должно быть поле, в котором хранятся сгенерированные разреженные векторы. В этом примере установите значение sparse.

index_type

Тип создаваемого индекса. Для полнотекстового поиска по алгоритму BM25 в Milvus установите для этого параметра значение SPARSE_INVERTED_INDEX. Дополнительную информацию см. в разделе SPARSE_INVERTED_INDEX.

metric_type

Значение этого параметра должно быть установлено на BM25 именно для обеспечения функциональности полнотекстового поиска.

params

Словарь дополнительных параметров, специфичных для данного индекса.

params.inverted_index_algo

Алгоритм, используемый для построения и запросов к разреженному инвертированному индексу BM25. Допустимые значения:

params.bm25_k1

Регулирует степень насыщения частоты терминов. Более высокие значения повышают значимость частоты терминов при ранжировании документов. Рекомендуемый диапазон: [1,2; 2,0]. Значение по умолчанию: 1,2.

params.bm25_b

Регулирует степень нормализации длины документов. Обычно используются значения от 0 до 1, значение по умолчанию — 0,75. Значение 0 означает отсутствие нормализации длины, а значение 1 — полную нормализацию длины.

Создание коллекции

Теперь создайте коллекцию, используя определённые параметры схемы и индекса.

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
}"

Вставка текстовых данных

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

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"
}'

После вставки данных в коллекцию вы можете выполнять полнотекстовый поиск с использованием запросов в виде необработанного текста. Milvus автоматически преобразует ваш запрос в разреженный вектор и ранжирует найденные результаты поиска с помощью алгоритма BM25, а затем возвращает topK (limit) результатов.

Вы можете выделять найденные термины в результатах поиска, настроив инструмент выделения текста. Подробности см. в разделе «Инструмент выделения текста ».

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,
)

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":{}
    }
}'

Параметр

Описание

search_params

Словарь, содержащий параметры поиска.

params.drop_ratio_search

Доля терминов с низкой значимостью, которые следует игнорировать при поиске. Значение должно находиться в диапазоне [0,0; 1,0). Подробности см. в разделе «Разреженный вектор».

data

Исходный текст запроса на естественном языке. Milvus автоматически преобразует текстовый запрос в разреженные векторы с помощью функции BM25 — не указывайте заранее вычисленные векторы.

anns_field

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

output_fields

Список имен полей, которые необходимо вернуть в результатах поиска. Поддерживаются все поля, кроме поля разреженных векторов, содержащего вложения, сгенерированные с помощью BM25. К типичным полям вывода относятся поле первичного ключа (например, id) и поле исходного текста (например, text). Дополнительную информацию см. в разделе «Часто задаваемые вопросы».

limit

Максимальное количество лучших совпадений для возврата.

Часто задаваемые вопросы

Нет, к разреженным векторам, сгенерированным функцией BM25, невозможно получить прямой доступ или вывести их результаты при полнотекстовом поиске. Подробности приведены ниже:

  • Функция BM25 внутренне генерирует разреженные векторы для ранжирования и поиска

  • Эти векторы хранятся в поле «sparse», но не могут быть включены в output_fields

  • Вы можете выводить только исходные текстовые поля и метаданные (такие как id, text)

Пример:

# ❌ This throws an error - you cannot output the sparse field
client.search(
    collection_name='my_collection', 
    data=['query text'],
    anns_field='sparse',
    output_fields=['text', 'sparse']  # 'sparse' causes an error
    limit=3,
    search_params=search_params
)

# ✅ This works - output text fields only
client.search(
    collection_name='my_collection', 
    data=['query text'],
    anns_field='sparse',
    output_fields=['text']
    limit=3,
    search_params=search_params
)

Зачем мне определять поле разреженных векторов, если я не могу получить к нему доступ?

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

Обоснование дизайна:

  • Разделение задач: вы работаете с текстом (ввод/вывод), а Milvus обрабатывает векторы (внутренняя обработка)

  • Производительность: заранее вычисленные разреженные векторы обеспечивают быстрое ранжирование по алгоритму BM25 при выполнении запросов

  • Пользовательский интерфейс: скрывает сложные векторные операции за простым текстовым интерфейсом

Если вам нужен доступ к векторам:

  • Используйте ручные операции с разреженными векторами вместо полнотекстового поиска

  • Создавайте отдельные коллекции для пользовательских рабочих процессов с разреженными векторами

Подробности см. в разделе «Редкие векторы».