Pencarian Teks Lengkap

Pencarian teks lengkap adalah fitur yang mengambil dokumen yang mengandung istilah atau frasa tertentu dalam kumpulan data teks, lalu memberi peringkat hasil berdasarkan relevansi. Fitur ini mengatasi keterbatasan pencarian semantik, yang mungkin mengabaikan istilah yang tepat, sehingga memastikan Anda menerima hasil yang paling akurat dan relevan secara kontekstual. Selain itu, fitur ini menyederhanakan pencarian vektor dengan menerima input teks mentah, secara otomatis mengubah data teks Anda menjadi sematan yang jarang tanpa perlu membuat sematan vektor secara manual.

Dengan menggunakan algoritme BM25 untuk penilaian relevansi, fitur ini sangat berharga dalam skenario retrieval-augmented generation (RAG), yang memprioritaskan dokumen yang sangat cocok dengan istilah pencarian tertentu.

Dengan mengintegrasikan pencarian teks lengkap dengan pencarian vektor padat berbasis semantik, Anda dapat meningkatkan akurasi dan relevansi hasil pencarian. Untuk informasi lebih lanjut, lihat Pencarian Hibrida.

Implementasi BM25

Milvus menyediakan pencarian teks lengkap yang didukung oleh algoritme relevansi BM25, fungsi penilaian yang diadopsi secara luas dalam sistem pencarian informasi, dan Milvus mengintegrasikannya ke dalam alur kerja pencarian untuk memberikan hasil teks yang akurat dan memiliki peringkat relevansi.

Pencarian teks lengkap di Milvus mengikuti alur kerja di bawah ini:

  1. Masukan teks mentah: Anda memasukkan dokumen teks atau memberikan kueri menggunakan teks biasa, tidak perlu model penyematan.

  2. Analisis teks: Milvus menggunakan penganalisis untuk memproses teks Anda menjadi istilah-istilah bermakna yang dapat diindeks dan dicari.

  3. Pemrosesan fungsi BM25: Fungsi bawaan mengubah istilah-istilah ini menjadi representasi vektor jarang yang dioptimalkan untuk penilaian BM25.

  4. Penyimpanan koleksi: Milvus menyimpan sematan jarang yang dihasilkan dalam koleksi untuk pengambilan dan pemeringkatan yang cepat.

  5. Penilaian relevansi BM25: Pada waktu pencarian, Milvus menerapkan fungsi penilaian BM25 untuk menghitung relevansi dokumen dan mengembalikan hasil peringkat yang paling sesuai dengan istilah kueri.

Full Text Search Pencarian Teks Lengkap

Untuk menggunakan pencarian teks lengkap, ikuti langkah-langkah utama berikut ini:

  1. Buat koleksi: Siapkan bidang yang diperlukan dan tentukan fungsi BM25 yang mengubah teks mentah menjadi sematan jarang.

  2. Memasukkan data: Memasukkan dokumen teks mentah Anda ke dalam koleksi.

  3. Melakukan pencarian: Gunakan teks kueri bahasa alami untuk mengambil hasil peringkat berdasarkan relevansi BM25.

Untuk mengaktifkan pencarian teks lengkap yang didukung BM25, Anda harus menyiapkan koleksi dengan bidang yang diperlukan, menetapkan fungsi BM25 untuk menghasilkan vektor jarang, mengonfigurasi indeks, lalu membuat koleksi.

Menentukan bidang skema

Skema koleksi Anda harus menyertakan setidaknya tiga bidang wajib:

  • Bidang utama: Mengidentifikasi secara unik setiap entitas dalam koleksi.

  • Bidang teks (VARCHAR): Menyimpan dokumen teks mentah. Harus menetapkan enable_analyzer=True agar Milvus dapat memproses teks untuk peringkat relevansi BM25. Secara default, Milvus menggunakan fitur standard analyzer untuk analisis teks. Untuk mengonfigurasi penganalisis yang berbeda, lihat Ikhtisar Penganalisis.

  • Bidang vektor jarang (SPARSE_FLOAT_VECTOR): Menyimpan sematan jarang yang dihasilkan secara otomatis oleh fungsi 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"
            }
        ]
    }'

Dalam konfigurasi sebelumnya,

  • id: berfungsi sebagai kunci utama dan secara otomatis dihasilkan dengan auto_id=True.

  • textmenyimpan data teks mentah Anda untuk operasi pencarian teks lengkap. Tipe datanya harus VARCHAR, karena VARCHAR adalah tipe data string Milvus untuk penyimpanan teks.

  • sparse: bidang vektor yang disediakan untuk menyimpan sematan jarang yang dihasilkan secara internal untuk operasi pencarian teks lengkap. Tipe data harus SPARSE_FLOAT_VECTOR.

Tentukan fungsi BM25

Fungsi BM25 mengubah teks yang diberi token menjadi vektor jarang yang mendukung penilaian BM25.

Tentukan fungsi dan tambahkan ke skema Anda:

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

Parameter

Deskripsi

name

Nama fungsi. Fungsi ini mengonversi teks mentah Anda dari bidang text menjadi vektor jarang yang kompatibel dengan BM25 yang akan disimpan di bidang sparse.

input_field_names

Nama bidang VARCHAR yang membutuhkan konversi teks ke vektor jarang. Untuk FunctionType.BM25, parameter ini hanya menerima satu nama bidang.

output_field_names

Nama bidang di mana vektor jarang yang dihasilkan secara internal akan disimpan. Untuk FunctionType.BM25, parameter ini hanya menerima satu nama bidang.

function_type

Jenis fungsi yang akan digunakan. Harus FunctionType.BM25.

Jika beberapa bidang VARCHAR memerlukan pemrosesan BM25, tentukan satu fungsi BM25 per bidang, masing-masing dengan nama dan bidang keluaran yang unik.

Mengonfigurasi indeks

Setelah mendefinisikan skema dengan bidang yang diperlukan dan fungsi bawaan, siapkan indeks untuk koleksi Anda.

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

Parameter

Deskripsi

field_name

Nama bidang vektor yang akan diindeks. Untuk pencarian teks lengkap, ini harus menjadi bidang yang menyimpan vektor jarang yang dihasilkan. Dalam contoh ini, tetapkan nilainya ke sparse.

index_type

Jenis indeks yang akan dibuat. AUTOINDEX memungkinkan Milvus mengoptimalkan pengaturan indeks secara otomatis. Jika Anda membutuhkan kontrol lebih besar atas pengaturan indeks Anda, Anda dapat memilih dari berbagai jenis indeks yang tersedia untuk vektor jarang di Milvus. Untuk informasi lebih lanjut, lihat Indeks yang didukung di Milvus.

metric_type

Nilai untuk parameter ini harus diatur ke BM25 secara khusus untuk fungsionalitas pencarian teks lengkap.

params

Kamus parameter tambahan khusus untuk indeks.

params.inverted_index_algo

Algoritme yang digunakan untuk membangun dan menanyakan indeks. Nilai yang valid:

  • "DAAT_MAXSCORE" (default): Pemrosesan kueri Dokumen per Dokumen (DAAT) yang dioptimalkan menggunakan algoritme MaxScore. MaxScore memberikan kinerja yang lebih baik untuk nilai k yang tinggi atau kueri dengan banyak istilah dengan melewatkan istilah dan dokumen yang kemungkinan besar berdampak minimal. Hal ini dicapai dengan mempartisi istilah ke dalam kelompok penting dan tidak penting berdasarkan nilai dampak maksimumnya, dengan fokus pada istilah yang dapat berkontribusi pada hasil k teratas.

  • "DAAT_WAND": Pemrosesan kueri DAAT yang dioptimalkan menggunakan algoritme WAND. WAND mengevaluasi lebih sedikit dokumen yang terkena dampak dengan memanfaatkan nilai dampak maksimum untuk melewatkan dokumen yang tidak kompetitif, tetapi memiliki overhead per hit yang lebih tinggi. Hal ini membuat WAND lebih efisien untuk kueri dengan nilai k kecil atau kueri pendek, di mana melewatkan lebih memungkinkan.

  • "TAAT_NAIVE": Pemrosesan kueri dasar Term-at-a-Time (TAAT). Meskipun lebih lambat dibandingkan dengan DAAT_MAXSCORE dan DAAT_WAND, TAAT_NAIVE menawarkan keuntungan yang unik. Tidak seperti algoritme DAAT, yang menggunakan skor dampak maksimum yang di-cache yang tetap statis terlepas dari perubahan pada parameter koleksi global (avgdl), TAAT_NAIVE secara dinamis beradaptasi dengan perubahan tersebut.

params.bm25_k1

Mengontrol saturasi frekuensi istilah. Nilai yang lebih tinggi meningkatkan pentingnya frekuensi istilah dalam pemeringkatan dokumen. Rentang nilai: [1.2, 2.0].

params.bm25_b

Mengontrol sejauh mana panjang dokumen dinormalisasi. Nilai antara 0 dan 1 biasanya digunakan, dengan standar umum sekitar 0,75. Nilai 1 berarti tidak ada normalisasi panjang, sedangkan nilai 0 berarti normalisasi penuh.

Membuat koleksi

Sekarang buatlah koleksi menggunakan skema dan parameter indeks yang telah ditentukan.

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

Menyisipkan data teks

Setelah menyiapkan koleksi dan indeks, Anda siap memasukkan data teks. Dalam proses ini, Anda hanya perlu menyediakan teks mentah. Fungsi bawaan yang telah kita definisikan sebelumnya secara otomatis menghasilkan vektor jarang yang sesuai untuk setiap entri teks.

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

Setelah Anda memasukkan data ke dalam koleksi Anda, Anda dapat melakukan pencarian teks lengkap menggunakan kueri teks mentah. Milvus secara otomatis mengubah kueri Anda menjadi vektor yang jarang dan mengurutkan hasil pencarian yang cocok menggunakan algoritme BM25, lalu mengembalikan hasil topK (limit).

Anda dapat menyorot istilah yang cocok dalam hasil pencarian dengan mengonfigurasi penyorot teks. Lihat Penyorot Teks untuk detailnya.

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

Parameter

Deskripsi

search_params

Kamus yang berisi parameter pencarian.

params.drop_ratio_search

Proporsi istilah yang kurang penting untuk diabaikan selama pencarian. Untuk detailnya, lihat Vektor Jarang.

data

Teks kueri mentah dalam bahasa alami. Milvus secara otomatis mengubah kueri teks Anda menjadi vektor jarang menggunakan fungsi BM25 - jangan berikan vektor yang telah dihitung sebelumnya.

anns_field

Nama bidang yang berisi vektor jarang yang dihasilkan secara internal.

output_fields

Daftar nama bidang yang akan dikembalikan dalam hasil pencarian. Mendukung semua bidang kecuali bidang vektor jarang yang berisi sematan yang dihasilkan BM25. Bidang keluaran yang umum termasuk bidang kunci utama (misalnya, id) dan bidang teks asli (misalnya, text). Untuk informasi lebih lanjut, lihat Pertanyaan Umum.

limit

Jumlah maksimum kecocokan teratas yang akan dikembalikan.

PERTANYAAN UMUM

Tidak, vektor jarang yang dihasilkan oleh fungsi BM25 tidak dapat diakses atau dikeluarkan secara langsung dalam pencarian teks lengkap. Berikut ini detailnya:

  • Fungsi BM25 menghasilkan vektor jarang secara internal untuk pemeringkatan dan pengambilan

  • Vektor-vektor ini disimpan di bidang jarang tetapi tidak dapat dimasukkan ke dalam output_fields

  • Anda hanya dapat menampilkan bidang teks asli dan metadata (seperti id, text)

Contoh:

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

Mengapa saya perlu mendefinisikan bidang vektor jarang jika saya tidak dapat mengaksesnya?

Bidang vektor jarang berfungsi sebagai indeks pencarian internal, mirip dengan indeks basis data yang tidak berinteraksi langsung dengan pengguna.

Dasar Pemikiran Desain:

  • Pemisahan Masalah: Anda bekerja dengan teks (input/output), Milvus menangani vektor (pemrosesan internal)

  • Kinerja: Vektor jarang yang telah dihitung sebelumnya memungkinkan pemeringkatan BM25 yang cepat selama kueri

  • Pengalaman Pengguna: Mengabstraksikan operasi vektor yang kompleks di balik antarmuka teks yang sederhana

Jika Anda membutuhkan akses vektor:

  • Gunakan operasi vektor jarang manual alih-alih pencarian teks lengkap

  • Membuat koleksi terpisah untuk alur kerja vektor jarang khusus

Untuk detailnya, lihat Vektor Jarang.