Ricerca a testo completo

La ricerca full text è una funzione che recupera i documenti contenenti termini o frasi specifiche nei dataset di testo, classificando poi i risultati in base alla rilevanza. Questa funzione supera le limitazioni della ricerca semantica, che potrebbe trascurare termini precisi, garantendo la ricezione dei risultati più accurati e contestualmente rilevanti. Inoltre, semplifica le ricerche vettoriali accettando input di testo grezzo, convertendo automaticamente i dati testuali in embedding sparsi senza la necessità di generare manualmente embedding vettoriali.

Utilizzando l'algoritmo BM25 per il punteggio di rilevanza, questa funzione è particolarmente preziosa negli scenari di retrieval-augmented generation (RAG), dove dà priorità ai documenti che corrispondono strettamente 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 della ricerca. Per ulteriori informazioni, consultare la sezione Ricerca ibrida.

Implementazione di BM25

Milvus offre una ricerca full text basata sull'algoritmo di rilevanza BM25, una funzione di punteggio ampiamente adottata nei sistemi di information retrieval, e Milvus la integra nel flusso di ricerca per fornire risultati di testo accurati e classificati per rilevanza.

La ricerca di testi completi in Milvus segue il seguente flusso di lavoro:

  1. Inserimento del testo grezzo: L'utente inserisce documenti di testo o fornisce una query utilizzando testo semplice, senza bisogno di modelli di incorporamento.

  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 rade ottimizzate per il punteggio BM25.

  4. Memorizzazione delle collezioni: Milvus memorizza le incorporazioni rade risultanti in una raccolta per un rapido recupero e classificazione.

  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 meglio corrispondono ai termini della query.

Full Text Search Ricerca a testo completo

Per utilizzare la ricerca full text, seguire i seguenti passaggi principali:

  1. Creare una raccolta: Impostare i campi richiesti e definire una funzione BM25 che converta il testo grezzo in embedding sparsi.

  2. Inserire i dati: Inserire i documenti di testo grezzo nella raccolta.

  3. Eseguire ricerche: Utilizzare il testo della query in linguaggio naturale per recuperare i risultati classificati in base alla rilevanza di BM25.

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

Definire i campi dello schema

Lo schema della raccolta deve includere almeno tre campi obbligatori:

  • Campo primario: Identifica in modo univoco ogni entità della raccolta.

  • Campo testo (VARCHAR): Memorizza documenti di testo grezzo. Deve essere impostato enable_analyzer=True in modo che Milvus possa elaborare il testo per la classificazione di rilevanza BM25. Per impostazione predefinita, Milvus utilizza l'analizzatore standard per l' analisi del testo. Per configurare un analizzatore diverso, fare riferimento a Panoramica dell'analizzatore.

  • Campo vettoriale sparso (SPARSE_FLOAT_VECTOR): Memorizza le incorporazioni rade generate 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: serve come chiave primaria ed è generato automaticamente con auto_id=True.

  • text: memorizza i dati del testo grezzo per le operazioni di ricerca full text. Il tipo di dati deve essere VARCHAR, poiché VARCHAR è il tipo di dati stringa di Milvus per la memorizzazione del testo.

  • sparse: un campo vettoriale riservato alla memorizzazione delle incorporazioni sparse generate internamente per le operazioni di ricerca full text. Il tipo di dati deve essere SPARSE_FLOAT_VECTOR.

Definire la 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 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 del campo text in vettori sparsi compatibili con BM25 che saranno memorizzati nel campo sparse.

input_field_names

Il nome del campo VARCHAR che richiede la conversione del testo in vettori sparsi. 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 solo un nome di campo.

function_type

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

Se più campi di VARCHAR richiedono l'elaborazione di BM25, definire una funzione BM25 per ogni campo, ciascuna con un nome e un campo di uscita unici.

Configurare l'indice

Dopo aver definito lo schema con i campi necessari e la funzione incorporata, configurare l'indice per la collezione.

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. AUTOINDEX permette a Milvus di ottimizzare automaticamente le impostazioni dell'indice. Se si desidera un maggiore controllo sulle impostazioni degli indici, è possibile scegliere tra i vari tipi di indice disponibili per i vettori sparsi in Milvus. Per ulteriori informazioni, consultare la sezione Indici supportati in Milvus.

metric_type

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

params

Un dizionario di parametri aggiuntivi specifici dell'indice.

params.inverted_index_algo

L'algoritmo usato per costruire e interrogare l'indice. Valori validi:

  • "DAAT_MAXSCORE" (predefinito): Elaborazione ottimizzata delle query Document-at-a-Time (DAAT) con l'algoritmo MaxScore. MaxScore fornisce prestazioni migliori per valori elevati di k o per query con molti termini, saltando termini e documenti che potrebbero avere un impatto minimo. Questo risultato si ottiene suddividendo i termini in gruppi essenziali e non essenziali in base ai punteggi di impatto massimo, concentrandosi sui termini che possono contribuire ai risultati top-k.

  • "DAAT_WAND": Elaborazione ottimizzata delle query DAAT con l'algoritmo WAND. WAND valuta un minor numero di documenti trovati, sfruttando i punteggi di impatto massimo per saltare i documenti non competitivi, ma ha un overhead più elevato per ogni singolo colpo. Questo rende WAND più efficiente per le query con valori k piccoli o per le query brevi, dove il salto è più fattibile.

  • "TAAT_NAIVE": Elaborazione di query Basic Term-at-a-Time (TAAT). Pur essendo più lento rispetto a DAAT_MAXSCORE e DAAT_WAND, TAAT_NAIVE offre un vantaggio unico. A differenza degli algoritmi DAAT, che utilizzano punteggi di impatto massimo memorizzati nella cache che rimangono statici indipendentemente dalle modifiche del parametro di raccolta globale (avgdl), TAAT_NAIVE si adatta dinamicamente a tali modifiche.

params.bm25_k1

Controlla la saturazione della frequenza dei termini. Valori più alti aumentano l'importanza delle frequenze dei termini nella classificazione dei documenti. Intervallo di valori: [1.2, 2.0].

params.bm25_b

Controlla la misura in cui la lunghezza del documento viene normalizzata. In genere si utilizzano valori compresi tra 0 e 1, con un valore predefinito comunemente intorno a 0,75. Un valore di 1 significa nessuna normalizzazione della lunghezza, mentre un valore di 0 significa normalizzazione completa.

Creare l'insieme

Creare ora l'insieme utilizzando i parametri dello schema e dell'indice 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" \
-d "{
    \"collectionName\": \"my_collection\",
    \"schema\": $schema,
    \"indexParams\": $indexParams
}"

Inserire i dati di testo

Dopo aver impostato la collezione e l'indice, si è pronti a inserire i dati di testo. In questo processo, è sufficiente fornire il testo grezzo. La funzione integrata definita in precedenza genera automaticamente il vettore sparse 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" \
-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 raccolta, è possibile eseguire ricerche full text utilizzando query di testo grezzo. Milvus converte automaticamente la query in un vettore sparse e classifica i risultati della ricerca utilizzando l'algoritmo BM25, per poi restituire i risultati topK (limit).

È possibile evidenziare i termini corrispondenti nei risultati della ricerca configurando un evidenziatore di testo. Per maggiori informazioni, vedere 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" \
--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

Percentuale di termini di scarsa importanza da ignorare durante la ricerca. Per i dettagli, fare riferimento a Vettore sparso.

data

Testo grezzo della query in linguaggio naturale. Milvus converte automaticamente la query di testo in vettori sparsi usando 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 quello dei vettori sparsi che contengono le incorporazioni generate da BM25. I campi di output più comuni includono il campo della chiave primaria (ad esempio, id) e il campo del testo originale (ad esempio, text). Per ulteriori informazioni, consultare le FAQ.

limit

Numero massimo di top match da restituire.

FAQ

No, i vettori sparsi generati dalla funzione BM25 non sono direttamente accessibili o visualizzabili nella ricerca full text. Ecco i dettagli:

  • La funzione BM25 genera internamente vettori sparsi per la classificazione e il recupero.

  • Questi vettori sono memorizzati nel campo sparse, ma non possono essere inclusi nella ricerca full text. output_fields

  • È possibile produrre 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 vettoriale sparse se non posso accedervi?

Il campo vettoriale sparse serve come indice di ricerca interno, simile agli indici dei database con cui gli utenti non interagiscono direttamente.

Motivazione del progetto:

  • Separazione delle preoccupazioni: L'utente lavora con il testo (input/output), Milvus gestisce i vettori (elaborazione interna).

  • Prestazioni: I vettori sparsi precalcolati permettono di classificare velocemente BM25 durante le interrogazioni.

  • Esperienza utente: Astrazione di complesse operazioni vettoriali dietro una semplice interfaccia testuale.

Se avete bisogno di accedere ai vettori:

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

  • Creare collezioni separate per flussi di lavoro vettoriali sparsi personalizzati

Per maggiori dettagli, consultare Sparse Vector.