Ricerca full-text

La ricerca full-text è una funzionalità che recupera i documenti contenenti termini o frasi specifici all'interno di set di dati testuali, classificando poi i risultati in base alla rilevanza. Questa funzionalità supera i limiti della ricerca semantica, che potrebbe trascurare termini precisi, garantendo risultati più accurati e contestualmente rilevanti. Inoltre, semplifica le ricerche vettoriali accettando l’input di testo grezzo, convertendo automaticamente i dati testuali in embedding sparsi senza la necessità di generare manualmente gli embedding vettoriali.

Utilizzando l’algoritmo BM25 per il punteggio di rilevanza, questa funzionalità è particolarmente utile negli scenari di generazione potenziata dal recupero (RAG), dove dà priorità ai documenti che corrispondono da vicino a termini di ricerca specifici.

Integrando la ricerca full-text con la ricerca vettoriale densa basata sulla semantica, è possibile migliorare l’accuratezza e la pertinenza dei risultati di ricerca. Per ulteriori informazioni, consultare Ricerca ibrida.

Implementazione di BM25

Milvus offre la ricerca full-text basata sull’algoritmo di rilevanza BM25, una funzione di valutazione ampiamente adottata nei sistemi di recupero delle informazioni, e la integra nel flusso di lavoro di ricerca per fornire risultati testuali accurati e ordinati per rilevanza.

La ricerca full-text in Milvus segue il flusso di lavoro riportato di seguito:

  1. Inserimento del testo grezzo: si inseriscono documenti di testo o si effettua una query utilizzando testo semplice, senza bisogno di modelli di embedding.

  2. Analisi del testo: Milvus utilizza un analizzatore per elaborare il testo in termini significativi che possono essere indicizzati e ricercati.

  3. Elaborazione della funzione BM25: una funzione integrata trasforma questi termini in rappresentazioni vettoriali sparse ottimizzate per il punteggio BM25.

  4. Archiviazione nella collezione: Milvus archivia gli embedding sparsi risultanti in una collezione per un recupero e un ordinamento rapidi.

  5. Punteggio di rilevanza BM25: al momento della ricerca, Milvus applica la funzione di punteggio BM25 per calcolare la rilevanza dei documenti e restituire i risultati classificati che corrispondono meglio ai termini della query.

Full Text Search Ricerca full-text

Per utilizzare la ricerca full-text, segui questi passaggi principali:

  1. Creazione di una collezione: configurare i campi richiesti e definire una funzione BM25 che converta il testo grezzo in embedding sparsi.

  2. Inserire i dati: importare i documenti di testo grezzo nella collezione.

  3. Eseguire ricerche: utilizzare query in linguaggio naturale per recuperare risultati ordinati in base alla rilevanza BM25.

Per abilitare la ricerca full-text basata su BM25, è necessario preparare una collezione con i campi richiesti, definire una funzione BM25 per generare vettori sparsi, configurare un indice e quindi creare la collezione.

Definire i campi dello schema

Lo schema della raccolta deve includere almeno tre campi obbligatori:

  • Campo primario: identifica in modo univoco ogni entità nella raccolta.

  • Campo stringa (VARCHAR o TEXT): memorizza i documenti di testo grezzi. È necessario impostare enable_analyzer=True in modo che Milvus possa elaborare il testo per il ranking di rilevanza BM25. Per impostazione predefinita, Milvus utilizza l’ standard analizzatore per l’analisi del testo. Per configurare un analizzatore diverso, consultare la Panoramica degli analizzatori. Gli esempi in questa pagina utilizzano VARCHAR; per testi lunghi, è possibile definire il campo di input come TEXT e omettere max_length. Per un esempio completo, consultare Campo di testo.

  • Campo vettoriale sparso (SPARSE_FLOAT_VECTOR): memorizza gli embedding sparsi generati automaticamente dalla funzione 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"
            }
        ]
    }'

Nella configurazione precedente,

  • id: funge da chiave primaria e viene generato automaticamente con auto_id=True.

  • text: memorizza i dati di testo grezzi per le operazioni di ricerca full-text. Il campo può utilizzare VARCHAR per testi di dimensioni limitate o TEXT per contenuti di origine di grandi dimensioni.

  • sparse: un campo vettoriale riservato alla memorizzazione di embedding sparsi generati internamente per le operazioni di ricerca full-text. Il tipo di dati deve essere SPARSE_FLOAT_VECTOR.

Definizione della funzione BM25

La funzione BM25 converte il testo tokenizzato in vettori sparsi che supportano il punteggio BM25.

Definire la funzione e aggiungerla allo schema:

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

Parametro

Descrizione

name

Il nome della funzione. Questa funzione converte il testo grezzo dal campo " text " in vettori sparsi compatibili con BM25 che verranno memorizzati nel campo " sparse ".

input_field_names

Il nome del campo VARCHAR o TEXT per il quale è richiesta la conversione da testo a vettore sparso. Per FunctionType.BM25, questo parametro accetta solo un nome di campo.

output_field_names

Il nome del campo in cui verranno memorizzati i vettori sparsi generati internamente. Per FunctionType.BM25, questo parametro accetta un solo nome di campo.

function_type

Il tipo di funzione da utilizzare. Deve essere FunctionType.BM25.

Se più campi di testo richiedono l’elaborazione BM25, definire una funzione BM25 per ogni campo, ciascuna con un nome e un campo di output univoci.

Configurare l’indice

Dopo aver definito lo schema con i campi necessari e la funzione integrata, configurare l’indice per la propria raccolta.

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

Parametro

Descrizione

field_name

Il nome del campo vettoriale da indicizzare. Per la ricerca full-text, questo dovrebbe essere il campo che memorizza i vettori sparsi generati. In questo esempio, impostare il valore su sparse.

index_type

Il tipo di indice da creare. Per la ricerca full-text BM25 in Milvus, impostare questo valore su SPARSE_INVERTED_INDEX. Per ulteriori informazioni, consultare SPARSE_INVERTED_INDEX.

metric_type

Il valore di questo parametro deve essere impostato su BM25 specificatamente per la funzionalità di ricerca full-text.

params

Un dizionario di parametri aggiuntivi specifici per l’indice.

params.inverted_index_algo

L'algoritmo utilizzato per la creazione e l'interrogazione dell'indice invertito sparso BM25. Valori validi:

params.bm25_k1

Controlla la saturazione della frequenza dei termini. Valori più elevati aumentano l’importanza delle frequenze dei termini nel ranking dei documenti. Intervallo consigliato: [1,2; 2,0]. Valore predefinito: 1,2.

params.bm25_b

Controlla il grado di normalizzazione della lunghezza dei documenti. In genere si utilizzano valori compresi tra 0 e 1, con un valore predefinito di 0,75. Un valore pari a 0 indica l’assenza di normalizzazione della lunghezza, mentre un valore pari a 1 indica la normalizzazione completa della lunghezza.

Creazione della raccolta

Ora crea la collezione utilizzando lo schema e i parametri di indicizzazione definiti.

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

Inserire dati di testo

Dopo aver configurato la collezione e l’indice, si è pronti per inserire i dati di testo. In questa fase, è sufficiente fornire il testo grezzo. La funzione integrata definita in precedenza genera automaticamente il vettore sparso corrispondente per ogni voce di testo.

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

Una volta inseriti i dati nella collezione, è possibile eseguire ricerche full-text utilizzando query di testo grezzo. Milvus converte automaticamente la query in un vettore sparso e ordina i risultati della ricerca corrispondenti utilizzando l’algoritmo BM25, per poi restituire i primi K (limit) risultati.

È possibile evidenziare i termini corrispondenti nei risultati di ricerca configurando un evidenziatore di testo. Per ulteriori dettagli, consultare la sezione Evidenziatore di testo.

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

Parametro

Descrizione

search_params

Un dizionario contenente i parametri di ricerca.

params.drop_ratio_search

Proporzione di termini di bassa importanza da ignorare durante la ricerca. Il valore deve essere compreso nell'intervallo [0,0; 1,0). Per ulteriori dettagli, consultare la sezione " Vettore sparso".

data

Testo grezzo della query in linguaggio naturale. Milvus converte automaticamente la query testuale in vettori sparsi utilizzando la funzione BM25: non fornire vettori precalcolati.

anns_field

Il nome del campo che contiene i vettori sparsi generati internamente.

output_fields

Elenco dei nomi dei campi da restituire nei risultati della ricerca. Supporta tutti i campi tranne il campo del vettore sparso contenente gli embedding generati da BM25. I campi di output comuni includono il campo della chiave primaria (ad es., id) e il campo del testo originale (ad es., text). Per ulteriori informazioni, consultare le FAQ.

limit

Numero massimo di corrispondenze principali da restituire.

Domande frequenti

No, i vettori sparsi generati dalla funzione BM25 non sono direttamente accessibili né esportabili nella ricerca full-text. Ecco i dettagli:

  • La funzione BM25 genera internamente vettori sparsi per il ranking e il recupero

  • Questi vettori sono memorizzati nel campo sparso ma non possono essere inclusi in output_fields

  • È possibile esportare solo i campi di testo originali e i metadati (come id, text)

Esempio:

# ❌ 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
)

Perché devo definire un campo vettore sparso se non posso accedervi?

Il campo vettore sparso funge da indice di ricerca interno, simile agli indici dei database con cui gli utenti non interagiscono direttamente.

Motivazioni progettuali:

  • Separazione dei livelli: l’utente si occupa del testo (input/output), mentre Milvus gestisce i vettori (elaborazione interna)

  • Prestazioni: i vettori sparsi precalcolati consentono un rapido ranking BM25 durante le query

  • Esperienza utente: nasconde le complesse operazioni vettoriali dietro una semplice interfaccia testuale

Se è necessario accedere ai vettori:

  • Utilizza operazioni manuali sui vettori sparsi invece della ricerca full-text

  • Crea raccolte separate per flussi di lavoro personalizzati con vettori sparsi

Per ulteriori dettagli, consultare la sezione " Vettori sparsi".